API documentation

Sanctions screening webhooks: verify and process events

Receive screening and monitoring events, check their signatures, and process each event once. Fetch the saved result when you need its evidence.

Updated

250 free live screenings. One lifetime allowance. No credit card required.
Receiver
Public HTTPS

Register an endpoint your application controls.

Verify
Raw body first

Verify the signature before parsing the payload.

Deduplicate
Event ID

Store processed IDs before acknowledging.

Create an endpoint and store the one-time secret

Use a key with webhooks:write. Replace the example URL with a public HTTPS receiver you control. POST /webhooks accepts only url and returns HTTP 201 with data.id, environment, url and secret. Store secret securely before discarding the response; GET /webhooks does not return it. Each environment permits at most five active endpoints.

Endpoint creation does not use the screening idempotency contract. If the response is lost, list endpoints and reconcile by URL before creating another. If a created endpoint’s secret was lost, disable it and register a replacement. There is no secret retrieval or rotation endpoint. Endpoint registration subscribes to supported events for that environment; an events filter is not accepted.

Terminal · curl
curl --fail-with-body "https://www.sanctionskit.com/api/v1/webhooks" \
  -H "Authorization: Bearer $SANCTIONSKIT_API_KEY" \
  -H 'Content-Type: application/json' \
  --data '{
  "url": "https://example.com/webhooks/sanctionskit"
}'

Verify the exact bytes with Node.js built-ins

Save this as verify-webhook.mjs. It uses the literal signing-secret string, not a Base64-decoded key. The signed bytes are Webhook-Id + a period + Webhook-Timestamp + a period + the raw request body. The timestamp is Unix seconds; reject signatures more than 300 seconds from your server clock in either direction.

Read the body once as bytes before any JSON middleware reserializes it. The header is v1= followed by 64 lowercase hexadecimal characters. Validate the length and format before using a constant-time comparison. A wrong secret, changed whitespace, altered payload or stale timestamp must fail authentication.

JavaScript · verify-webhook.mjs
// Save as verify-webhook.mjs. Uses only Node.js built-in crypto.
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(secret, headers, rawBody, now = Math.floor(Date.now() / 1000)) {
  const id = headers.get('Webhook-Id') ?? '';
  const timestamp = headers.get('Webhook-Timestamp') ?? '';
  const signature = headers.get('Webhook-Signature') ?? '';
  if (!secret || !id || !/^\d+$/.test(timestamp) ||
      !Number.isSafeInteger(Number(timestamp)) ||
      Math.abs(now - Number(timestamp)) > 300 ||
      !/^v1=[a-f0-9]{64}$/.test(signature)) return false;
  // Use the secret as the literal string returned on endpoint creation.
  const expected = createHmac('sha256', secret)
    .update(id + '.' + timestamp + '.').update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(signature.slice(3), 'hex'));
}

Acknowledge only after durable acceptance

This Web Request adapter verifies before parsing and accepts a persistence callback from your application. Implement persistEventAndEnqueue as one durable transaction: insert a unique receipt for endpoint, environment and event ID, and enqueue the work only on the first insert. A duplicate receipt succeeds without enqueueing again. Keep the receipt through your redelivery handling period.

Return a 2xx response promptly after that commit. A storage failure must yield 5xx so the delivery can be retried. Configure your framework’s request-body limit and execute slow result retrieval in the queued worker. Do not use an in-memory Set for production deduplication or acknowledge before work is durably accepted.

JavaScript · receiver adapter
import { verifyWebhook } from './verify-webhook.mjs';

// Framework adapter: pass its unparsed Web Request and your durable transaction.
export async function receiveWebhook(request, persistEventAndEnqueue) {
  const rawBody = Buffer.from(await request.arrayBuffer());
  if (!verifyWebhook(process.env.SANCTIONSKIT_WEBHOOK_SECRET, request.headers, rawBody)) {
    return new Response('Invalid signature', { status: 400 });
  }
  let event;
  try { event = JSON.parse(rawBody.toString('utf8')); }
  catch { return new Response('Invalid JSON', { status: 400 }); }
  if (event?.id !== request.headers.get('Webhook-Id') ||
      !['sandbox', 'production'].includes(event.environment) ||
      typeof event.type !== 'string' || !event.data) {
    return new Response('Invalid event', { status: 400 });
  }
  // Atomically insert a UNIQUE(endpoint, environment, event.id) receipt
  // and queue work. An already-recorded event is a successful no-op.
  // A database failure must throw/return 5xx so delivery can be retried.
  await persistEventAndEnqueue(event);
  return new Response(null, { status: 204 });
}

Understand event data and delivery order

Event envelopes contain id, type, environment, data and createdAt. screening.completed uses data.screeningId and data.status. monitoring events carry opaque monitor/event/counterparty/screening/case references when available, kind and errorCode. They omit subject names and matched evidence. Retrieve /results/{id} or /monitoring/inbox/{eventId} with a separate results:read credential when your worker needs detail.

Do not assume ordered or exactly-once delivery. A repeated event ID is the same event even if its delivery timestamp/signature changes. Ignore unsupported event types after durable recording, and reconcile authoritative current resources when events arrive out of order. A screening event signals a screening outcome; it does not approve a transaction or finish human review.

Inspect delivery failures and retry deliberately

GET /webhooks returns data.endpoints and data.deliveries, each bounded to the latest 100 entries; it is not a cursor-paginated complete delivery history. A delivery has its own id, endpoint_id and event_id. Use the delivery id, not the endpoint or event id, for POST /webhooks/{id}/retry.

A successful retry request returns HTTP 202 with data.id, environment and status pending. It only schedules another attempt. Inspect attempts, last_status, status and next_attempt_at to diagnose delivery. Transport failures can record last_status 0; non-2xx receiver responses fail delivery. Retries are bounded, so failed work needs operational attention and current-state reconciliation.

DELETE /webhooks/{endpointId} disables the endpoint and returns data.id, environment and active:false. A retry against a disabled endpoint returns 409 webhook_disabled. Fix the receiver before retrying; never remove signature verification to make deliveries appear successful.

Terminal · curl
curl --fail-with-body "https://www.sanctionskit.com/api/v1/webhooks" \
  -H "Authorization: Bearer $SANCTIONSKIT_API_KEY"

Register a public HTTPS receiver

POST /api/v1/webhooks registers a destination and returns its signing secret once. Store that secret in server configuration. Destinations must use public HTTPS on port 443; private, local, reserved, and redirected destinations are rejected. DNS validation has a three-second deadline and checks every returned address. Delivery revalidates the destination, pins the checked address, and has a ten-second total deadline. Do not include a credential in the destination URL.

screening.completed events contain an event ID, environment, screening ID, status, and creation time. They omit the submitted subject. Fetch the authorized result separately if your handler needs it. Webhook delivery history and retries are available in the dashboard.

monitoring.initial_match, match_added, match_removed, evidence_changed, policy_changed, baseline_unavailable, screening_failed, screening_overdue, screening_recovered and review_required use the monitoring. prefix. Each event includes opaque monitor, event, counterparty, screening and case references where available, plus kind and errorCode. Names and matched evidence are omitted. Retrieve the authorized inbox event to inspect retained evidence; a notification is not an automatic business decision.

Request and response reference

Frequently asked questions

Why must webhook verification use the raw request body?

The signature covers the exact body bytes with the event ID and timestamp. Parsing and reformatting JSON can change those bytes. Verify Webhook-Id, Webhook-Timestamp, and Webhook-Signature with the raw body before trusting or acting on the payload.

Can the same webhook event arrive more than once?

Yes. Save verified event IDs and the intended work before acknowledging delivery. A repeated event must not create a second business action. A new delivery timestamp does not make the same event ID a new event.

Do screening webhooks contain the submitted person’s name?

No. Events use IDs and status information instead of submitted subject details. Use an authorized API request to fetch the retained result when needed. Monitoring events also omit names and matched evidence; a notification is not a business decision.

Can I register a localhost URL as the webhook destination?

No. Destinations must use public HTTPS on port 443. Private, local, reserved, and redirected destinations are rejected, and delivery rechecks the address. Keep credentials out of the destination URL and store the one-time signing secret on your server.

KEEP BUILDING

Where to go next