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
Authorization: All batch targets must be domains you own or are authorized to assess — see the Acceptable Use Policy.
Quota: Batch operations are metered per domain. Scanning 10 domains in a batch uses 10 of your monthly scans, and comparing 3 domains uses 3. Batch endpoints are available on every plan, including Free.
Batch scores are not full-scan scores: The batch engine runs four modules (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:

POST/batch/scan

Submit up to 50 domains for parallel scanning. Returns aggregated statistics and individual domain results.

Request Body

ParameterTypeRequiredDescription
domainsstring[]YesArray of domains to scan (1-50 domains)
sort_byenumNoSort 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:

POST/batch/compare

Compare 2-5 domains with side-by-side module scoring and ranked results. Ideal for before/after assessments across your own domains.

Request Body

ParameterTypeRequiredDescription
domainsstring[]YesArray 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/limits before 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 first
  • sort_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

OperationMinimumMaximum
Batch Scan1 domain50 domains
Batch Compare2 domains5 domains
Plan Requirements: Batch endpoints are available on every plan, including Free. The limit is your monthly scan quota, not your tier — a batch of 20 domains needs 20 scans remaining. Check /v1/usage/limits first if you are close to your quota.

Related Topics