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.
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.
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.
// 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.
{"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.
// 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.
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"}
JSONStorage 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
Base URL: https://www.sanctionskit.com/api/v1. Open an operation for its complete headers, body fields, response formats, examples and error codes.
- GET
/batchesList batches with progress - POST
/batchesAccept up to 100 inline requests or an authorized private upload - GET
/batches/{id}Read batch and row outcomes - DELETE
/batches/{id}Cancel pending batch rows - POST
/uploads/intentAuthorize a direct private Blob upload - POST
/uploadsCreate a client token for an authorized private batch upload - GET
/files/{id}Download a private file after tenant authorizationSchemas:Error
Browse the complete API reference or download the OpenAPI contract.
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