Batch Operations
Scan or compare multiple domains simultaneously for efficient security assessments across your entire portfolio. Perfect for MSPs, agencies, and organizations managing many domains.
Why Use Batch Operations?
- Efficiency: Scan up to 50 domains in a single API request
- Comparison: Side-by-side analysis of 2-5 domains
- Portfolio Overview: Quickly assess security across all assets
- Benchmarking: Compare domains you own or manage, side by side
dns_email, ssl_tls, http_headers, open_ports) and averages them without weighting, on an A-F scale of 90/80/70/60. A full scan runs 15 modules with materiality weights and grade bands of 90/75/55/35. The same domain will often score differently under each. Use batch results to triage a portfolio, then run a full scan on the domains that matter.Batch Scanning
Scan multiple domains in parallel and get aggregated results:
/batch/scanSubmit up to 50 domains for parallel scanning. Returns aggregated statistics and individual domain results.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
domains | string[] | Yes | Array of domains to scan (1-50 domains) |
sort_by | enum | No | Sort results by: risk, score, or domain (default: risk) |
curl -X POST "https://api.compliancelayer.net/v1/batch/scan" \
-H "Authorization: Bearer cl_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domains": [
"example.com",
"example.org",
"example.net",
"test-site.com",
"demo-app.io"
],
"sort_by": "risk"
}'Response
{
"total": 5,
"summary": {
"avg_score": 78,
"critical_risk": 1,
"passing": 3,
"grade_distribution": {
"A": 1,
"B": 2,
"C": 1,
"D": 0,
"F": 1
}
},
"results": [
{
"domain": "test-site.com",
"overall_score": 42,
"overall_grade": "F",
"modules": {
"dns_email": { "score": 65, "grade": "D", "issues": 2 },
"ssl_tls": { "score": 0, "grade": "F", "issues": 3 },
"http_headers": { "score": 55, "grade": "F", "issues": 4 },
"open_ports": { "score": 50, "grade": "F", "issues": 2 }
},
"error": null
},
{
"domain": "example.com",
"overall_score": 87,
"overall_grade": "B",
"modules": {
"dns_email": { "score": 82, "grade": "B", "issues": 1 },
"ssl_tls": { "score": 95, "grade": "A", "issues": 0 },
"http_headers": { "score": 88, "grade": "B", "issues": 1 },
"open_ports": { "score": 84, "grade": "B", "issues": 0 }
},
"error": null
}
]
}Results are sorted by risk (lowest score first) by default. Each domain reports the four batch modules; modules[name].issues is an issue count, not a list, and avg_score is an integer. A module that failed to run is omitted from modules rather than reported as zero. Grades are A through F with no plus or minus variants.
Requesting more than 50 domains (or fewer than 1) is rejected with 422 Unprocessable Entity before any scanning happens.
Domain Comparison
Compare 2-5 domains side-by-side to benchmark security posture:
/batch/compareCompare 2-5 domains with side-by-side module scoring and ranked results. Ideal for before/after assessments across your own domains.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
domains | string[] | Yes | Array of domains to compare (2-5 domains) |
curl -X POST "https://api.compliancelayer.net/v1/batch/compare" \
-H "Authorization: Bearer cl_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domains": [
"www.example.com",
"app.example.com",
"mail.example.com"
]
}'Response
{
"ranked": [
{
"rank": 1,
"domain": "www.example.com",
"overall_score": 92,
"overall_grade": "A"
},
{
"rank": 2,
"domain": "app.example.com",
"overall_score": 78,
"overall_grade": "C"
},
{
"rank": 3,
"domain": "mail.example.com",
"overall_score": 65,
"overall_grade": "D"
}
],
"winner": "www.example.com",
"score_gap": 27,
"module_comparison": {
"dns_email": {
"www.example.com": 90,
"app.example.com": 75,
"mail.example.com": 60
},
"ssl_tls": {
"www.example.com": 95,
"app.example.com": 82,
"mail.example.com": 70
},
"http_headers": {
"www.example.com": 88,
"app.example.com": 72,
"mail.example.com": 58
},
"open_ports": {
"www.example.com": 95,
"app.example.com": 83,
"mail.example.com": null
}
},
"full_results": []
}winner is simply the highest-scoring domain in the set — it carries no judgement beyond the ranking. score_gap is the spread between the highest and lowest ranked domain. module_comparison always contains all four batch modules; a domain whose module did not return data appears as null. full_results repeats the same per-domain objects returned by /batch/scan.
Practical Examples
MSP Portfolio Assessment
Scan all client domains and generate a security report:
async function assessClientPortfolio(clientDomains) {
// Split into batches of 50
const batches = [];
for (let i = 0; i < clientDomains.length; i += 50) {
batches.push(clientDomains.slice(i, i + 50));
}
// Scan each batch
const allResults = [];
for (const batch of batches) {
const response = await fetch(
'https://api.compliancelayer.net/v1/batch/scan',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
domains: batch,
sort_by: 'risk'
})
}
);
const batchResults = await response.json();
allResults.push(...batchResults.results);
// Respect rate limits - wait 1 second between batches
await new Promise(r => setTimeout(r, 1000));
}
// Analyze results
const criticalDomains = allResults.filter(r => r.overall_score < 60);
const passingDomains = allResults.filter(r => r.overall_score >= 80);
console.log(`Portfolio Assessment Complete`);
console.log(`Total domains: ${allResults.length}`);
console.log(`Critical risk: ${criticalDomains.length}`);
console.log(`Passing: ${passingDomains.length}`);
console.log(`\nDomains requiring attention:`);
criticalDomains.forEach(domain => {
console.log(`- ${domain.domain}: ${domain.overall_grade}`);
});
return {
total: allResults.length,
critical: criticalDomains,
passing: passingDomains,
all: allResults
};
}
// Usage
const clients = [
'client-a.com',
'client-b.com',
'client-c.com',
// ... up to 50+ domains
];
const report = await assessClientPortfolio(clients);Cross-Property Consistency Check
Compare a primary domain against the other properties you own to find where configuration has drifted:
async function consistencyCheck(primaryDomain, otherProperties) {
const domains = [primaryDomain, ...otherProperties];
const response = await fetch(
'https://api.compliancelayer.net/v1/batch/compare',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ domains })
}
);
const comparison = await response.json();
// Where does the primary domain sit in the set?
const primary = comparison.ranked.find(r => r.domain === primaryDomain);
console.log(`Primary domain (${primaryDomain}):`);
console.log(`Rank: #${primary.rank} out of ${comparison.ranked.length}`);
console.log(`Score: ${primary.overall_score} (${primary.overall_grade})`);
// Find modules where the primary domain lags the rest of the portfolio
console.log(`\nModule Analysis:`);
Object.entries(comparison.module_comparison).forEach(([module, scores]) => {
const primaryScore = scores[primaryDomain];
if (primaryScore === null) return; // module returned no data for this domain
// Modules that failed to run come back as null - exclude them from the average
const otherScores = Object.entries(scores)
.filter(([domain, score]) => domain !== primaryDomain && score !== null)
.map(([, score]) => score);
if (otherScores.length === 0) return;
const portfolioAvg =
otherScores.reduce((sum, score) => sum + score, 0) / otherScores.length;
if (primaryScore < portfolioAvg) {
console.log(`⚠️ ${module}: behind the rest of the portfolio (${primaryScore} vs avg ${portfolioAvg.toFixed(0)})`);
}
});
return comparison;
}
// Usage — all domains must be ones you own or are authorized to assess
const results = await consistencyCheck(
'www.example.com',
['app.example.com', 'mail.example.com', 'docs.example.com']
);Before/After Migration Testing
Compare old and new infrastructure during migrations:
async function migrationTest(oldDomain, newDomain) {
const response = await fetch(
'https://api.compliancelayer.net/v1/batch/compare',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
domains: [oldDomain, newDomain]
})
}
);
const comparison = await response.json();
const oldResult = comparison.full_results.find(r => r.domain === oldDomain);
const newResult = comparison.full_results.find(r => r.domain === newDomain);
console.log(`Migration Security Assessment`);
console.log(`
Old Infrastructure (${oldDomain}):`);
console.log(` Score: ${oldResult.overall_score}`);
console.log(`
New Infrastructure (${newDomain}):`);
console.log(` Score: ${newResult.overall_score}`);
const diff = newResult.overall_score - oldResult.overall_score;
if (diff > 0) {
console.log(`
✅ Security improved by ${diff} points`);
} else if (diff < 0) {
console.log(`
⚠️ Security degraded by ${Math.abs(diff)} points`);
} else {
console.log(`
➡️ No change in security score`);
}
// Module-level differences
console.log(`
Module Changes:`);
Object.keys(comparison.module_comparison).forEach(module => {
const oldScore = comparison.module_comparison[module][oldDomain];
const newScore = comparison.module_comparison[module][newDomain];
// A module that failed to run is reported as null on that domain
if (oldScore === null || newScore === null) return;
const change = newScore - oldScore;
if (change !== 0) {
const arrow = change > 0 ? '⬆' : '⬇';
console.log(` ${arrow} ${module}: ${oldScore} → ${newScore} (${change > 0 ? '+' : ''}${change})`);
}
});
return comparison;
}
// Usage
await migrationTest('old-site.example.com', 'new-site.example.com');Best Practices
1. Respect Rate Limits
- Each domain in a batch counts toward your monthly scan quota
- Scanning 50 domains uses 50 of your monthly scans
- Use
/usage/limitsbefore large batches to check remaining quota - For very large portfolios (>50 domains), split into multiple batches with delays
2. Optimize Batch Sizes
// Check quota before batch
async function smartBatchScan(domains) {
// Check remaining quota
const limitsResponse = await fetch(
'https://api.compliancelayer.net/v1/usage/limits',
{
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`
}
}
);
const limits = await limitsResponse.json();
const remaining = limits.usage.scans_remaining;
if (domains.length > remaining) {
console.warn(`⚠️ Batch size (${domains.length}) exceeds remaining quota (${remaining})`);
console.log(`Scanning only first ${remaining} domains`);
domains = domains.slice(0, remaining);
}
// Proceed with batch
return await batchScan(domains);
}3. Handle Errors Gracefully
Individual domain scans may fail without affecting others:
async function batchScanWithRetry(domains) {
const response = await fetch(
'https://api.compliancelayer.net/v1/batch/scan',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ domains })
}
);
const results = await response.json();
// Separate successful and failed scans
const successful = results.results.filter(r => r.error === null);
const failed = results.results.filter(r => r.error !== null);
if (failed.length > 0) {
console.log(`⚠️ ${failed.length} domains failed:`);
failed.forEach(result => {
console.log(` - ${result.domain}: ${result.error}`);
});
// Optionally retry failed domains
if (failed.length < 5) {
console.log('Retrying failed domains...');
const retryDomains = failed.map(r => r.domain);
// ... retry logic
}
}
return { successful, failed };
}4. Use Appropriate Sorting
sort_by: "risk"— Prioritize critical issues (default)sort_by: "score"— Show best performers firstsort_by: "domain"— Alphabetical order
5. Cache Comparison Results
Batch comparisons are computationally expensive. Cache results for at least 1 hour:
const cache = new Map();
async function cachedCompare(domains) {
const cacheKey = domains.sort().join(',');
// Check cache
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey);
if (Date.now() - cached.timestamp < 3600000) { // 1 hour
console.log('Using cached comparison');
return cached.data;
}
}
// Fetch fresh data
const comparison = await compareDomains(domains);
// Cache result
cache.set(cacheKey, {
data: comparison,
timestamp: Date.now()
});
return comparison;
}Limitations
| Operation | Minimum | Maximum |
|---|---|---|
| Batch Scan | 1 domain | 50 domains |
| Batch Compare | 2 domains | 5 domains |
/v1/usage/limits first if you are close to your quota.Related Topics
- Running Scans - Individual domain scanning
- Usage Analytics - Monitor batch operation consumption
- Rate Limits - Understanding batch operation limits