Node.js SDK
The ComplianceLayer Node.js SDK is a TypeScript-first client for the API, with automatic retries on transient failures, typed responses, and helpers for polling scans to completion.
Overview
- TypeScript-first — every request and response is typed; declarations ship with the build
- Zero runtime dependencies — built on the global
fetch, no third-party packages - Node.js 18+ — requires the built-in
fetchandAbortSignal.timeout - CJS and ESM — both module formats are produced by the build
- Auto-retry — exponential backoff with jitter on 429 and 5xx responses
- camelCase responses — the API's snake_case JSON is converted for you
Installation
npm install compliancelayerQuickstart
scan() submits a job and returns immediately. scanAndWait() submits and polls until the scan finishes.
import { ComplianceLayer } from "compliancelayer";
const client = new ComplianceLayer({ apiKey: "cl_your_api_key_here" });
// Submit and wait for the result
const job = await client.scanAndWait("example.com");
console.log(job.domain, job.grade, job.score);
// Then pull the full report
const report = await job.getReport();
console.log(`${report.totalIssues} issues (${report.criticalIssues} critical)`);
for (const issue of report.issues.slice(0, 5)) {
console.log(`[${issue.severity}] ${issue.finding}`);
}Client Configuration
The constructor takes a single options object. apiKeyis required — the constructor throws if it is missing.
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | — (required) | Your API key, starting with cl_ |
baseUrl | string | https://api.compliancelayer.net | Override the API base URL. Give the host only — the SDK appends /v1/... itself |
timeout | number | 30000 | Per-request timeout in milliseconds |
pollInterval | number | 3000 | Milliseconds between polls while waiting for a scan |
pollTimeout | number | 120000 | Maximum milliseconds to wait for completion |
maxRetries | number | 3 | Retry attempts on 429 and 5xx responses |
fetch | typeof fetch | globalThis.fetch | Injectable fetch implementation, useful in tests |
import { ComplianceLayer } from "compliancelayer";
const client = new ComplianceLayer({
apiKey: process.env.COMPLIANCELAYER_API_KEY!,
timeout: 60_000,
pollInterval: 5_000,
pollTimeout: 300_000,
maxRetries: 5,
});/v1 or /api to baseUrl. The client builds paths like /v1/scan/ on top of whatever you pass, so adding a prefix yourself produces URLs such as /v1/v1/scan/ and every call 404s.Scanning
| Method | Returns | Description |
|---|---|---|
scan(domain) | ScanJob | Submit a scan and return immediately, without polling |
scanAndWait(domain, options?) | ScanJob | Submit and poll until the scan completes |
waitForCompletion(jobId, options?) | ScanJob | Poll an existing job to a terminal state |
getJob(jobId) | ScanJobResponse | Fetch the current status of a job |
getReport(jobId) | ScanReport | Fetch the full report for a completed job |
freeScan(domain) | FreeScanResult | Unauthenticated, rate-limited scan with top findings only |
history(options?) | ScanHistoryResponse | Recent scans for the account; accepts { limit } |
scan() returns a ScanJob whose score and grade are undefined until the scan completes. Both are getters over job.result.
const job = await client.scan("example.com");
console.log(job.jobId, job.status); // e.g. 12345 "queued"
// Poll manually
while (!job.isComplete) {
await new Promise((r) => setTimeout(r, 3000));
await job.refresh();
}
if (job.status === "failed") {
console.error("Scan failed:", job.failureReason);
} else {
const report = await job.getReport();
console.log(report.grade, report.score);
}Batch Operations
batchScan() scans up to 50 domains in one request; batchCompare() ranks domains against each other. Both return per-domain summaries with module scores rather than full reports — call getReport() for the detail. See Batch Operations.
const batch = await client.batchScan([
"example.com",
"example.org",
"example.net",
]);
console.log(`${batch.total} domains scanned`);
console.log(batch.summary);
for (const result of batch.results) {
if (result.error) {
console.warn(`${result.domain}: ${result.error}`);
} else {
console.log(result.domain, result.overallGrade, result.overallScore);
}
}Domain Monitoring
Domain operations live under client.domains. Note that request bodies use the API's snake_case field names, while responses come back camelCased.
| Method | Returns | Description |
|---|---|---|
domains.create(data) | Domain | Register a domain for monitoring |
domains.list() | DomainListResponse | Monitored domains plus limitUsed / limitMax |
domains.delete(id) | void | Stop monitoring a domain, by numeric ID |
domains.scan(id) | DomainScanJobResponse | Trigger an immediate scan of a monitored domain |
domains.alerts() | DomainAlert[] | Alerts across monitored domains |
alert_threshold is the minimum score decrease that triggers an alert — a delta, not an absolute score. scan_frequency accepts "hourly", "daily", or "weekly".
const domain = await client.domains.create({
domain: "example.com",
scan_frequency: "daily",
alert_on_score_drop: true,
alert_threshold: 5, // alert on a drop of 5+ points
});
console.log(domain.id, domain.domain, domain.scanFrequency);
const listing = await client.domains.list();
console.log(`${listing.limitUsed} of ${listing.limitMax} domains in use`);
for (const d of listing.domains) {
console.log(d.id, d.domain, d.lastScore, d.lastGrade);
}
await client.domains.delete(domain.id);Webhooks
Webhook operations live under client.webhooks. See the Webhooks guide for event payloads and signature verification.
| Method | Returns | Description |
|---|---|---|
webhooks.create(data) | Webhook | Create an endpoint; the response includes the signing secret |
webhooks.list() | Webhook[] | All endpoints on the account |
webhooks.get(id) | Webhook | Fetch a single endpoint |
webhooks.update(id, data) | Webhook | PATCH; send only the fields you want changed — url, enabled_events, description, or is_active |
webhooks.delete(id) | void | Delete an endpoint |
webhooks.test(id) | WebhookTestResult | Send a test payload to the endpoint |
webhooks.deliveries(id) | WebhookDelivery[] | Delivery history for an endpoint |
const endpoint = await client.webhooks.create({
url: "https://example.com/hooks/compliancelayer",
enabled_events: ["scan.completed", "alert.triggered"],
description: "Production listener",
});
// The signing secret is only returned on creation -- store it now.
console.log(endpoint.id, endpoint.secret);
const test = await client.webhooks.test(endpoint.id);
console.log(test.success, test.statusCode, test.responseTimeMs);
for (const delivery of await client.webhooks.deliveries(endpoint.id)) {
console.log(delivery.createdAt, delivery.eventType, delivery.status);
console.log(" HTTP", delivery.httpStatusCode, "retries:", delivery.retryCount);
if (delivery.errorMessage) console.log(" error:", delivery.errorMessage);
}
// update() issues a PATCH -- send only the fields you want to change
await client.webhooks.update(endpoint.id, { is_active: false });crypto module — the Webhooks guide has ready-to-copy verification code.Badges
badgeSvgUrl() and badgeJsonUrl() build URLs without making a request; getBadge() fetches the badge JSON. See Security Badges.
badgeSvgUrl() returns an unauthenticated endpoint, so it is the one you can embed in a README or a public page — but badges are opt-in, so it serves 404 until the domain's owner has published a badge. The JSON endpoint requires authentication — getBadge() sends your API key, but the raw URL from badgeJsonUrl() will not load for anonymous visitors. Use it for server-side calls, not as a public link.// URL builders -- no network call
const svgUrl = client.badgeSvgUrl("example.com");
console.log(``); // public: safe to embed in a README
// Authenticated fetch of the badge payload
const badge = await client.getBadge("example.com");
console.log(badge.domain, badge.grade, badge.score);
// Same endpoint as getBadge(), so it needs an API key to load
const jsonUrl = client.badgeJsonUrl("example.com");Account Info
me() returns the authenticated account and its quota.
const account = await client.me();
console.log(account.email, account.plan);
console.log(account.scansThisMonth, "of", account.scanLimit, "scans used");
console.log(account.scansRemaining, "remaining");
console.log("Domain limit:", account.domainLimit);
// The endpoint does not report domains in use -- read it from the list:
const { limitUsed } = await client.domains.list();
console.log(limitUsed, "domains in use");Error Handling
Every HTTP error class extends APIError, which carries status and body. Catching APIError catches all of them. ScanTimeoutError is the one exception: it extends Error, because no HTTP response is involved.
| Class | Thrown when |
|---|---|
APIError | Base class; also thrown directly for unmapped status codes |
AuthenticationError | HTTP 401 — the API key is missing or invalid |
ForbiddenError | HTTP 403 — your plan does not permit this operation |
NotFoundError | HTTP 404 — the resource does not exist |
ValidationError | HTTP 409 or 422 — the request was rejected as invalid |
QuotaExceededError | HTTP 429 where the response indicates the scan quota is exhausted |
RateLimitError | HTTP 429 after retries are exhausted; exposes retryAfter |
ScanError | The scan failed server-side |
ScanTimeoutError | Polling exceeded the poll timeout; exposes jobId |
import {
ComplianceLayer,
APIError,
AuthenticationError,
QuotaExceededError,
RateLimitError,
ScanError,
ScanTimeoutError,
ValidationError,
} from "compliancelayer";
const client = new ComplianceLayer({ apiKey: "cl_your_api_key_here" });
try {
const job = await client.scanAndWait("example.com");
console.log(job.grade, job.score);
} catch (error) {
if (error instanceof AuthenticationError) {
console.error("Check your API key.");
} else if (error instanceof QuotaExceededError) {
console.error("Monthly scan quota exhausted.");
} else if (error instanceof RateLimitError) {
console.error(`Rate limited; retry after ${error.retryAfter}s`);
} else if (error instanceof ValidationError) {
console.error("Invalid request:", error.message);
} else if (error instanceof ScanTimeoutError) {
console.error(`Job ${error.jobId} did not finish in time.`);
} else if (error instanceof ScanError) {
console.error("Scan failed:", error.message);
} else if (error instanceof APIError) {
console.error(`API error ${error.status}:`, error.message);
} else {
throw error;
}
}maxRetries. RateLimitError is only thrown once those retries are exhausted; QuotaExceededError is thrown immediately, since waiting will not help.TypeScript Types
All response types are exported for use in your own signatures.
import type {
ComplianceLayerOptions,
ScanJobResponse,
ScanReport,
ScanHistoryResponse,
FreeScanResult,
BatchScanResponse,
BatchCompareResponse,
DomainScanResult,
RankedDomain,
Domain,
DomainListResponse,
DomainAlert,
Webhook,
WebhookDelivery,
WebhookTestResult,
BadgeJson,
AccountInfo,
Finding,
ModuleResult,
} from "compliancelayer";
function summarize(report: ScanReport): string {
return `${report.domain}: ${report.grade} (${report.score})`;
}Testing
The fetch option lets you inject a stub instead of hitting the network.
const fakeFetch: typeof fetch = async () =>
new Response(JSON.stringify({ job_id: 1, domain: "example.com", status: "queued" }), {
status: 200,
headers: { "content-type": "application/json" },
});
const client = new ComplianceLayer({
apiKey: "cl_test_key",
fetch: fakeFetch,
});