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 fetch and AbortSignal.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 compliancelayer

Quickstart

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.

OptionTypeDefaultDescription
apiKeystring— (required)Your API key, starting with cl_
baseUrlstringhttps://api.compliancelayer.netOverride the API base URL. Give the host only — the SDK appends /v1/... itself
timeoutnumber30000Per-request timeout in milliseconds
pollIntervalnumber3000Milliseconds between polls while waiting for a scan
pollTimeoutnumber120000Maximum milliseconds to wait for completion
maxRetriesnumber3Retry attempts on 429 and 5xx responses
fetchtypeof fetchglobalThis.fetchInjectable 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,
});
Do not append /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

MethodReturnsDescription
scan(domain)ScanJobSubmit a scan and return immediately, without polling
scanAndWait(domain, options?)ScanJobSubmit and poll until the scan completes
waitForCompletion(jobId, options?)ScanJobPoll an existing job to a terminal state
getJob(jobId)ScanJobResponseFetch the current status of a job
getReport(jobId)ScanReportFetch the full report for a completed job
freeScan(domain)FreeScanResultUnauthenticated, rate-limited scan with top findings only
history(options?)ScanHistoryResponseRecent 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.

MethodReturnsDescription
domains.create(data)DomainRegister a domain for monitoring
domains.list()DomainListResponseMonitored domains plus limitUsed / limitMax
domains.delete(id)voidStop monitoring a domain, by numeric ID
domains.scan(id)DomainScanJobResponseTrigger 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.

MethodReturnsDescription
webhooks.create(data)WebhookCreate an endpoint; the response includes the signing secret
webhooks.list()Webhook[]All endpoints on the account
webhooks.get(id)WebhookFetch a single endpoint
webhooks.update(id, data)WebhookPATCH; send only the fields you want changed — url, enabled_events, description, or is_active
webhooks.delete(id)voidDelete an endpoint
webhooks.test(id)WebhookTestResultSend 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 });
Verifying webhook signatures. The SDK does not export a signature helper. Verify the HMAC-SHA256 signature yourself using Node's 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.

Only the SVG URL is public. 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(`![Security](${svgUrl})`);   // 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.

ClassThrown when
APIErrorBase class; also thrown directly for unmapped status codes
AuthenticationErrorHTTP 401 — the API key is missing or invalid
ForbiddenErrorHTTP 403 — your plan does not permit this operation
NotFoundErrorHTTP 404 — the resource does not exist
ValidationErrorHTTP 409 or 422 — the request was rejected as invalid
QuotaExceededErrorHTTP 429 where the response indicates the scan quota is exhausted
RateLimitErrorHTTP 429 after retries are exhausted; exposes retryAfter
ScanErrorThe scan failed server-side
ScanTimeoutErrorPolling 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;
  }
}
Retries are automatic. The client retries 429 and 5xx responses with exponential backoff plus jitter, up to 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,
});

Next Steps