Developer guide
OFAC API in Node.js: backend customer screening
Build a Node.js OFAC API check with native fetch. Keep keys on the server, retry one saved operation, and send SDN matches to review.

On this page
Add OFAC screening to a Node.js backend
Place the provider request in a server-only module and let your application authenticate the user before accepting a screening operation. Validate the subject and permitted coverage on the backend. A browser should call your application boundary, where the SanctionsKit credential and source policy remain under your control.
Persist the customer reference, exact request and operation key before awaiting fetch. If the browser disconnects, the backend still needs to reconcile the operation. Return an application status that distinguishes a completed comparison from review work and an incomplete call; do not expose raw candidate evidence through a generic error response.
Prepare the synthetic exercise
Use the invented Mira Calder request to exercise an onboarding route before connecting customer records. Create a sandbox key in API keys, then save the operation before the route awaits the provider. Closing the browser must not create an untraceable screening.
| Requirement | Value or action |
|---|---|
| Runtime | Use Node.js 22 or later on a trusted server or CLI. This implementation uses the built-in fetch client and Node standard-library modules, so no SDK or npm dependency is required. |
| Client file | Save the client as screen.mjs. |
| Screening endpoint | POST /api/v1/screenings |
| SANCTIONSKIT_API_KEY | Sandbox key; screenings:write and results:read scopes |
| SCREENING_OPERATION_ID | Persist one operation ID; reuse only with the identical request body |
| request.json | Mira Calder: invented subject; sandbox@1: synthetic records |
{
"subject": {
"name": "Mira Calder",
"entityType": "person",
"birthDate": "1984"
},
"package": "sandbox@1",
"retention": "standard"
}Implement the Node.js request boundary
The Node.js client uses native fetch and checks production source discovery before it posts a subject. Its abort signal bounds local waiting; it is not evidence that remote work was cancelled. Keep the operation outside the request handler so a returning user can reopen its saved state.
// Node.js 22+. Run on your trusted backend; no npm dependencies.
import { readFileSync } from 'node:fs';
const base = 'https://www.sanctionskit.com/api/v1';
async function main() {
const key = process.env.SANCTIONSKIT_API_KEY;
const operation = process.env.SCREENING_OPERATION_ID;
if (!key || !operation) throw new Error('Set your key and operation ID.');
const request = JSON.parse(readFileSync(process.argv[2] ?? 'request.json', 'utf8'));
const sandbox = request.package === 'sandbox@1';
const sourceIds = sandbox ? ['sandbox-synthetic'] : request.sources;
if (!Array.isArray(sourceIds) || !sourceIds.length) throw new Error('Select your sources.');
async function api(path, body) {
const response = await fetch(base + path, {
method: body ? 'POST' : 'GET', redirect: 'error', signal: AbortSignal.timeout(25000),
headers: { Authorization: 'Bearer ' + key,
...(body ? { 'Content-Type': 'application/json', 'Idempotency-Key': operation } : {}) },
...(body ? { body: JSON.stringify(body) } : {}),
});
const envelope = await response.json();
if (!response.ok) {
const code = /^[a-z0-9_]{1,80}$/.test(envelope?.error?.code ?? '') ? envelope.error.code : 'unknown';
throw new Error('API HTTP ' + response.status + ': ' + code);
}
return envelope?.data;
}
if (!sandbox) {
const sources = await api('/sources');
if (!Array.isArray(sources)) throw new Error('Invalid discovery response.');
for (const id of sourceIds) {
const found = sources.filter(source => source?.id === id);
const source = found[0];
if (found.length !== 1 || source.availability !== 'available' || source.fresh !== true ||
!Array.isArray(source.capabilities) || !source.capabilities.includes(request.subject.entityType)) throw new Error('Required source unavailable or incompatible.');
}
}
const result = await api('/screenings', request);
if (typeof result?.id !== 'string' || !result.id || result.environment !== (sandbox ? 'sandbox' : 'production') ||
!['potential_match', 'no_match'].includes(result.status) ||
!Array.isArray(result.matches) || (result.status === 'no_match') !== (result.matches.length === 0) ||
!Array.isArray(result.coverage) || result.coverage.length !== sourceIds.length ||
sourceIds.some(id => result.coverage.filter(item => item.sourceId === id && item.fresh === true).length !== 1)) {
throw new Error('Incomplete screening result.');
}
// Store result.id with your customer reference; use results:read to retrieve the evidence.
console.log(JSON.stringify({ id: result.id, status: result.status,
candidateCount: result.matches.length, sources: result.coverage.map(item => item.sourceId) }));
}
await main().catch(error => {
console.error((/^API HTTP [0-9]{3}: [a-z0-9_]{1,80}$/.test(error.message) ||
error.message === 'Required source unavailable or incompatible.')
? error.message : 'Screening incomplete. Check credentials, request and source coverage.');
process.exitCode = 1;
});Run the client
node screen.mjs request.jsonConfirm OFAC scope before production
For an SDN onboarding step, select ofac-sdn explicitly. A policy that requires OFAC non-SDN lists needs a separate ofac-non-sdn selection and review of its different measures. The server, rather than a browser-supplied display label, should own that coverage choice.
| Check | Required evidence |
|---|---|
| GET /api/v1/sources | Production key with sources:read; response data array |
| id | ofac-sdn |
| availability / fresh | available / true |
| capabilities | Contains the requested subject.entityType |
| currentVersion | Active version; retrieved_at and last_checked_at are not publication dates |
| lastImport | May describe an import that is not the active version |
| Missing or ineligible source | Inspect disabledReason; required coverage stays incomplete |
| Displayed client | Performs production discovery before screening |
curl --fail-with-body --max-time 25 --header "Authorization: Bearer $SANCTIONSKIT_API_KEY" "https://www.sanctionskit.com/api/v1/sources"Select ofac-sdn in the production request
| Setting | Production request |
|---|---|
| Key | Separate production credential with screenings:write |
| Coverage | sources: ["ofac-sdn"]; omit package |
| Operation ID | New ID for the changed body and environment |
| Usage | Production access and screening allowance apply |
| Subject | Only lawfully held, known identifying facts; no invented date precision |
{
"subject": {
"name": "Mira Calder",
"entityType": "person",
"birthDate": "1984"
},
"sources": [
"ofac-sdn"
],
"retention": "standard"
}Interpret OFAC evidence in this workflow
Attach data.id to the onboarding record and use data.coverage to show what the completed operation checked. Route data.matches into the authorized evidence view. A no_match outcome should finish this comparison step without quietly marking ownership review or customer approval complete.
| Fields in data.matches[] | Review use |
|---|---|
| record.sourceId and record.id | Keep the SDN source and stable record reference attached to the candidate. |
| record.designations[].programs and legalReferences | Show the listed program and legal references; do not turn every non-SDN restriction into an asset freeze. |
| record.names, identifiers and birthDates | Compare the supplied identity with aliases and available identifiers, preserving partial-date precision. |
- data.id; data.coverage; data.versions; supplied disclaimer
- data.matches[]: record, assessment, evidence, conflicts
- GET /api/v1/results/{id}: results:read scope
- Retained evidence and case review
Verify the application behavior
| Condition | Next action |
|---|---|
| 401 / 403 | Correct credentials or scopes before retrying |
| coverage_unavailable / stale_coverage | Keep required coverage incomplete; no silent source substitution |
| Timeout after submission | Recover with the original Idempotency-Key and identical body |
| 409 | Investigate an operation-key conflict |
| 429 / 503 | Follow the specific error and Retry-After; an allowance cap may require a plan or usage change |
Contract and safe diagnostic references
| Reference | Use |
|---|---|
| error.code / error.requestId | Safe recovery diagnostics; never log the submitted subject, key or full candidate response |
| potential_match / no_match | Completed comparison outcomes; neither is an automatic business approval |
- Disconnect the browser after submission and reopen the same saved operation.
- Mock stale_coverage and verify the onboarding state remains incomplete.
- Return a potential_match and verify the browser receives a review state rather than an automatic approval.
Sources and references
Authority and technical sources
SanctionsKit documentation
Frequently asked questions
Can an OFAC Node.js route use fetch without installing an SDK?
Yes. This client targets Node.js 22 or later and uses native fetch. Keep the call behind your authenticated server route; the browser does not need the service credential or the full candidate response.
Where should a Node.js onboarding service choose SDN versus non-SDN coverage?
Put the approved source IDs in server-side workflow configuration. The displayed production request selects SDN only. A customer request must not remove required non-SDN coverage simply to avoid a source-availability error.
What if the browser closes while my Node.js OFAC request is running?
The backend still needs to recover the operation. Save its request, customer reference, and key before awaiting fetch. When the user returns, show the saved operation state instead of sending another check with a new key.
What does closing the browser prove about an OFAC request?
It proves only that the user stopped waiting. The provider may have accepted the screening. Recover the persisted operation using its original request and key before deciding whether a new comparison is needed.