Developer guide
EU sanctions API in Node.js: customer screening
Use the EU sanctions API from Node.js to check financial-sanctions records. Preserve source IDs, partial dates and regulation references for review.

On this page
Screen EU customers from a Node.js backend
Call the client from the server route that receives an onboarding request. Store the local operation ID before submitting the subject, then save the returned screening ID against that customer. Return a pending-review state when a candidate needs investigation.
Keep the API key on the server. Use the retained result to display EU record references and evidence to authorized reviewers; the browser only needs your application’s screening summary.
Prepare the synthetic exercise
Run the invented customer through sandbox before attaching the Node.js handler to onboarding. Your local fixture should include a potential match and an incomplete response, with different application states. Neither should advance the customer merely because fetch returned a JSON object.
| 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
This Node.js integration calls SanctionsKit rather than downloading EU source files. Native fetch performs discovery and screening, while the backend controls authentication, operation persistence and which evidence an authorized reviewer may see.
// 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 EU sanctions scope before production
eu-consolidated selects the EU consolidated financial sanctions list. It is a designated-party check, not a complete test of sectoral, goods or trade restrictions. Confirm the source’s current availability before the production request instead of assuming a tutorial establishes eligibility.
| Check | Required evidence |
|---|---|
| GET /api/v1/sources | Production key with sources:read; response data array |
| id | eu-consolidated |
| 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 eu-consolidated in the production request
| Setting | Production request |
|---|---|
| Key | Separate production credential with screenings:write |
| Coverage | sources: ["eu-consolidated"]; 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": [
"eu-consolidated"
],
"retention": "standard"
}Interpret EU sanctions evidence in this workflow
Keep the EU candidate’s regulation reference beside data.id and data.coverage. Route unresolved identities into the review workflow. Your application can show a concise onboarding state while retaining the fuller source evidence for authorized investigation.
| Fields in data.matches[] | Review use |
|---|---|
| record.id and record.sourceId | Save the EU record identity with the screening result instead of indexing the returned names yourself. |
| record.names, identifiers and birthDates | Show primary names, aliases and available identifiers; keep a year or range as published. |
| record.designations[].legalReferences and programs | Read the applicable regulation and regime context beside the candidate. |
- 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 |
- Route a mocked EU candidate to pending review instead of approval.
- Fail an incomplete response without rendering an empty match list.
- Verify that the browser receives only the authorized application summary.
Sources and references
Authority and technical sources
SanctionsKit documentation
Frequently asked questions
Is an EU XML parser required for this Node.js guide?
No. The guide uses the SanctionsKit API for selected-source screening. Your Node.js backend submits the known subject, validates the response and connects retained evidence to review; it does not operate an EU import pipeline.
Does the EU Node.js result cover sectoral and goods restrictions?
The selected source is the consolidated financial sanctions list. Other restrictions relevant to a transaction need their own assessment. Do not describe a completed party-list comparison as a complete EU sanctions decision.
Must my Node.js application download and parse EU source files?
No. This integration submits a subject to SanctionsKit and reads the returned candidates and source versions. Your application handles the customer workflow and review. Use source discovery to confirm coverage instead of building a separate list parser for this example.
How should an EU onboarding route distinguish a candidate from failure?
A validated potential_match is completed screening with evidence to review. An HTTP, coverage or response-contract failure is incomplete work. Give these separate application states rather than returning one generic empty result.