API documentation

Batch sanctions screening API: submit and track a list

Send up to 100 subjects as JSON or upload a larger JSONL file. Track each row, inspect failures, and retry only the work that needs a new check.

Updated

250 free live screenings. One lifetime allowance. No credit card required.
Inline input
100 subjects

Maximum subjects in one inline request.

Private upload
25 MiB

JSONL files with up to 10,000 rows.

Acceptance
202 response

Track individual rows until completion.

Follow acceptance through to each row

1. Accepted
HTTP 202 gives you a batch ID. Processing or import may still fail. Save the ID and original operation key.
2. Processing stops
Poll until completed, failed or cancelled, then retrieve every row using nextOffset. Completed batches can contain failed rows.
3. Reconcile
Retrieve evidence for completed rows. Resolve failed or cancelled rows separately; never repeat the entire successful batch.

1. Accept an inline batch

Create a sandbox key with batches:write and results:read. Set BATCH_REQUEST_KEY to a new UUID and persist it with this exact request. Each row is a full ScreeningRequest, including its own coverage and optional reference or policy. The batch name is required; subjects and uploadId are mutually exclusive.

This request returns HTTP 202 with data.id, data.status and data.total only. Inline acceptance uses status pending and the submitted row count. Save data.id as BATCH_ID. Creation does not return row results or guarantee successful completion. Repeating creation with the same key and input returns the original acceptance, even after processing has finished.

Terminal · curl
curl --fail-with-body "https://www.sanctionskit.com/api/v1/batches" \
  -H "Authorization: Bearer $SANCTIONSKIT_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $BATCH_REQUEST_KEY" \
  --data '{
  "name": "Synthetic onboarding sample",
  "subjects": [
    {
      "subject": {
        "name": "Alex Morgan",
        "entityType": "person",
        "birthDate": "1984"
      },
      "package": "sandbox@1",
      "reference": "demo-customer-001",
      "retention": "standard"
    },
    {
      "subject": {
        "name": "Example Trading Company",
        "entityType": "organization"
      },
      "package": "sandbox@1",
      "reference": "demo-customer-002"
    }
  ]
}'

2. Poll progress and inspect each row

Poll GET /batches/{id}?limit=100&offset=0 with a bounded interval, for example every five seconds, and a timeout appropriate to your job. Use one coordinator for a batch instead of a polling loop per row. Follow Retry-After on rate or capacity errors. Stop polling terminal statuses completed, failed or cancelled; importing, pending and processing are still in progress.

The detail data object includes id, environment, name, status, total, completed, failed, cancelled_at, created_at, rows and nextOffset. Row keys are row_number, status, screening_id and error. Row status is pending, completed, failed or cancelled. A completed row has screening_id for GET /results/{id}; an unsuccessful row must never be interpreted as no_match. error is row-level diagnostic data, distinct from the HTTP error envelope.

A batch with status completed can contain failed rows. Reconcile completed + failed and cancelled rows with total. A failed import may have no rows and total 0; that is a failed upload/import, not an empty successfully screened portfolio. Retain the batch ID when requesting help with an import failure.

Terminal · curl
curl --fail-with-body "https://www.sanctionskit.com/api/v1/batches/$BATCH_ID?limit=100&offset=0" \
  -H "Authorization: Bearer $SANCTIONSKIT_API_KEY"

3. Read all result rows after processing stops

Batch row pagination uses a numeric offset, not the UUID cursor used to list batches. Send nextOffset exactly as returned until it is null. The default row limit is 25 and maximum is 100; offset can be 0 through 10,000. Do not stop because a page is smaller than the requested limit.

This function works with the helper in authentication. Once it returns, retrieve each completed row’s screening_id and record failed/cancelled rows separately. For very large downstream processing, persist one page at a time instead of retaining the array. Correct and resubmit only unsuccessful subjects with new request identities.

JavaScript · batch rows
// Call after the batch reaches completed, cancelled, or failed.
// A terminal batch can contain unsuccessful rows: inspect every row.status.
export async function readBatchRows(api, batchId) {
  const rows = [];
  let offset = 0;
  do {
    const page = await api('/batches/' + encodeURIComponent(batchId) +
      '?limit=100&offset=' + offset);
    if (!Array.isArray(page.rows)) throw new Error('Invalid batch page.');
    rows.push(...page.rows);
    const next = page.nextOffset;
    if (next === null) break;
    if (!Number.isInteger(next) || next <= offset) {
      throw new Error('Invalid batch continuation.');
    }
    offset = next;
  } while (true);
  return rows;
}

4. Build a private JSONL upload for larger portfolios

For more than 100 rows, create a UTF-8 .jsonl file with one complete ScreeningRequest JSON object per line, no surrounding array, no CSV header and no blank placeholder records. The following is a two-row synthetic file for testing the upload path. Every row uses standard retention and an explicit coverage selector. Maximum file size is 25 MiB and maximum row count is 10,000.

Save it as subjects.jsonl. Original PDFs, images and identity documents do not belong in this flow; link those through the governed case document workflow. The byte length, not the character count, is used for the storage reservation.

JSONL · subjects.jsonl
{"subject":{"name":"Alex Morgan","entityType":"person","birthDate":"1984"},"package":"sandbox@1","reference":"demo-customer-001","retention":"standard"}
{"subject":{"name":"Example Trading Company","entityType":"organization"},"package":"sandbox@1","reference":"demo-customer-002"}

5. Upload the bytes and retain the upload ID

Install @vercel/blob in your server-side Node project and save this as upload.mjs beside sk-api.mjs from the authentication guide. Run node upload.mjs subjects.jsonl. The script requests POST /uploads/intent, then uses the returned pathname and handleUploadUrl to obtain the upload token and transfer bytes directly to private storage. Never expose your SanctionsKit key in a browser bundle.

The intent returns HTTP 201 with data.id, pathname, access, handleUploadUrl, maxBytes, reservedBytes and format. POST /uploads is the upload library’s token handshake; its successful response is raw {type, clientToken}, without data. Let the library perform that exchange. Do not send the file bytes to POST /uploads or invent a PUT /files endpoint.

The signed storage-completion callback validates the uploaded content before it becomes ready. Upload transport success and batch availability are separate states. The upload ID is used once and expires after 24 hours. Preserve it across batch retries; repeatedly allocating intents consumes temporary storage reservations.

JavaScript · upload.mjs
// Save as upload.mjs. Install the upload transport: npm install @vercel/blob
// First save sk-api.mjs from the authentication guide in this directory.
import { readFile } from 'node:fs/promises';
import { upload } from '@vercel/blob/client';
import { api } from './sk-api.mjs';

const filename = process.argv[2];
if (!filename) throw new Error('Usage: node upload.mjs subjects.jsonl');
const bytes = await readFile(filename);
if (bytes.length < 1 || bytes.length > 26214400) {
  throw new Error('JSONL must contain 1 to 26,214,400 bytes.');
}
const intent = await api('/uploads/intent', {
  method: 'POST', body: { sizeBytes: bytes.length },
});
await upload(intent.pathname, bytes, {
  access: 'private',
  contentType: 'application/x-ndjson',
  multipart: false,
  handleUploadUrl: new URL(intent.handleUploadUrl, 'https://www.sanctionskit.com').href,
  headers: { Authorization: 'Bearer ' + process.env.SANCTIONSKIT_API_KEY },
  abortSignal: AbortSignal.timeout(120000),
});
// Persist this ID before batch submission; do not upload the file again on a retry.
console.log({ uploadId: intent.id });

6. Accept the uploaded batch and cancel pending work when needed

Set UPLOAD_ID to the ID printed by the upload script and BATCH_REQUEST_KEY to the operation UUID. The shell variables in this example are UUIDs. An uploaded batch is accepted with HTTP 202, status importing and total 0; import later determines total. Poll the batch detail resource as above.

If acceptance returns 409 upload_unavailable immediately after upload, the completion callback may still be pending. Retry the identical body and key with a bounded delay. The same code also covers expired, already-used and invalid/incomplete files, so stop after your deadline and investigate before uploading again. A failed upload or 503 temporarily_unavailable response is not a completed upload.

DELETE /batches/{id}, with batches:write, returns data.id and status. It cancels pending work and releases unused reservations; completed results remain retained and count toward usage. A completed or failed batch keeps that terminal state. GET /batches lists accepted batches using limit and nextCursor, independently from each batch’s row offsets.

Terminal · curl
curl --fail-with-body https://www.sanctionskit.com/api/v1/batches \
  -H "Authorization: Bearer $SANCTIONSKIT_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $BATCH_REQUEST_KEY" \
  --data @- <<JSON
{"name":"Synthetic uploaded sample","uploadId":"$UPLOAD_ID"}
JSON

Storage and retention requirements

Inline batches accept up to 100 complete screening requests. Batch rows use standard retention so potential matches can be reviewed. Minimal-retention rows are rejected. Failed rows do not count as completed screens.

Private bulk uploads and supporting text attachments use your organization’s file-storage allowance, including upload reservations. Database screening evidence and external document links do not count toward it. Original PDFs and images stay in your organization’s document service. If storage is full, no new file is accepted: wait for temporary uploads to expire or choose a plan with more storage. No automatic overage charges apply.

The upload limit is 25 MiB (26,214,400 bytes). An omitted sizeBytes reserves the full file limit for older integrations. Invalid encoding, null characters, unsupported formats and over-limit files are rejected.

Recover only unfinished subjects

Use the reconciliation steps above before preparing another batch. Keep completed evidence linked to its original screening ID; corrected failed subjects are new screening operations. For an uncertain acceptance, recover the original request before submitting anything new.

Request and response reference

Frequently asked questions

When should I use inline JSON instead of a JSONL batch upload?

Use an inline subjects array for up to 100 complete screening requests. For larger work, use the private JSONL upload flow: up to 10,000 rows and 25 MiB. Each row is its own screening request and uses standard retention.

Does HTTP 202 mean every batch subject has been screened?

No. It means the batch was accepted. Read progress and then inspect every row, following the returned pagination. A completed job can contain failed rows. Keep pending, failed, and cancelled rows separate from completed no-match results.

Should I resubmit the entire batch when one row fails?

Usually you need a new operation only for the rows that require correction or another check. Keep the original batch record and connect replacement rows to it. Resubmitting successful rows with new operation keys creates new screenings and usage.

What does cancelling a batch stop?

Cancellation stops pending work between processing chunks. Completed screenings remain in the record and still count toward usage. Unused reservations are released. Read the final row states so your system knows which subjects were actually completed.

KEEP BUILDING

Where to go next