Sanctions Screening API
Screens a person, organization, crypto wallet, bank account or transaction against sanctions, PEP and other risk lists. It returns matches, not verdicts — the decision is yours.
threshold and hit type is your policy — and that separation is what you defend in an audit.Authentication
The screening endpoints require a short-lived access token, obtained with an Ed25519 signature:
POST https://api-gateway.lunixi.com/api/v1/auth/token| Header | Required | Description |
|---|---|---|
X-Key-Id | ✅ | Your public key id (kid) |
X-Signature | ✅ | Ed25519 signature (Base64) |
X-Date | ✅ | ISO-8601 request timestamp |
X-Nonce | ✅ | Unique request nonce |
Digest | — | SHA-256 body digest (Base64); required when there is a body |
Send the returned token as Authorization: Bearer … on screening calls.
Your key must carry the sanction:view_results scope; a token minted from a key without it is rejected on the screening endpoints. Canonical string and error codes: Authentication.
Screening endpoints
Four subject types, four endpoints:
POST /api/v1/sanction/screening/name # person or organization
POST /api/v1/sanction/screening/wallet # crypto wallet address
POST /api/v1/sanction/screening/account # bank account / IBAN
POST /api/v1/sanction/screening/transaction # transaction (with its parties)Name, wallet and transaction screening, and public procurement ban screening, run against the active list publication: every screening pins the publication in force at that moment and tells you in the response which one it read. What a list publication is: Sanctions Screening.
Name screening body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✅ | The name to screen |
subjectType | string | — | person | organization |
country | string | — | ISO 3166-1 alpha-2 |
dateOfBirth | string | — | ISO-8601; cuts false positives sharply on person screening |
identifiers | array | — | Disambiguators such as national id, tax number, passport |
riskDomains | string[] | — | Narrows the screening to these risk domains: sanctions, pep, rca, wanted, regulatory, debarment, ownership, vessel, securities. When omitted, every risk domain is screened |
mode | string | — | sanctions_only | sanctions_pep | full_risk — a preset set of risk domains (see AML Screening). Sent together with riskDomains, their intersection is screened |
threshold | number | — | Minimum match score |
customerReference | string | — | Your own record id (written to the audit trail) |
curl -X POST "https://api-gateway.lunixi.com/api/v1/sanction/screening/name" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Mehmet Öz",
"subjectType": "person",
"country": "TR",
"dateOfBirth": "1980-04-12",
"riskDomains": ["sanctions", "pep", "rca"],
"threshold": 0.85,
"customerReference": "cust_84213"
}'An unknown mode or risk domain, or an empty intersection of mode and riskDomains, is refused with sanction.screening.scope_invalid (400). An invalid domain is never silently dropped — if it were, a domain nobody screened would read as "no match".
Response
The response comes in the standard response envelope; the screening fields are under data.
{
"status": "success",
"code": "sanction.screen_name.completed",
"message": "…",
"data": {
"latencyMs": 128,
"screeningRequestId": "…",
"publicationId": "…",
"publicationRevision": 34,
"coverageStatus": "COMPLETE",
"evidenceRef": "…",
"screenedRiskDomains": ["sanctions", "wanted", "regulatory", "debarment", "pep", "rca"],
"uncoveredRiskDomains": [],
"matches": [
{
"hitId": "…",
"entityId": "…",
"matchedName": "Mehmet Oz",
"score": 0.91,
"hitType": "probable",
"listCode": "OFAC SDN",
"sourceListName": "OFAC SDN",
"recordType": "person",
"riskDomains": ["sanctions"],
"reason": "PROBABLE_MATCH",
"explanationJson": "{…}",
"lists": [
{
"datasetCode": "OFAC",
"displayName": "OFAC SDN",
"datasetKind": "SANCTIONS",
"datasetVersionId": "…",
"subLists": [],
"programs": ["…"],
"entityIds": ["…"],
"listings": [
{ "subList": "", "label": "…", "program": "…", "reference": "…", "listedOn": "…" }
]
},
{
"datasetCode": "UN",
"displayName": "…",
"datasetKind": "SANCTIONS",
"datasetVersionId": "…",
"subLists": [],
"programs": ["…"],
"entityIds": ["…"],
"listings": []
}
]
}
],
"datasetFreshnessJson": "{…}"
},
"statusCode": 200,
"requestId": "…"
}| Field | Meaning |
|---|---|
matches | An empty array means clear only when status is "success" — and, when coverageStatus is not COMPLETE, only for the domains that were screened. See When screening cannot run |
score | Similarity between 0 and 1; anything under threshold is dropped |
hitType | exact | probable | possible |
riskDomains | Risk domains of the matched person or organization (sanctions, pep, wanted …) |
lists | Every list the match appears on — list code and name, the list version read, programs and sub-lists, listing records (listings) |
sourceListName | The list of the match's representative record. Read lists for all of them |
screeningRequestId | The screening's id; use it for screening history and report requests. The top-level requestId identifies the HTTP request, not the screening |
publicationId · publicationRevision | The list publication the screening read and its revision at screening time |
coverageStatus | COMPLETE | PARTIAL | DEGRADED — below |
screenedRiskDomains | The risk domains requested |
uncoveredRiskDomains | Requested risk domains no list in the publication covers |
evidenceRef | Id of the screening's immutable evidence record; use it in audits and disputes |
datasetFreshnessJson | Which vintage of data you screened against. "We screened PEP" alone is not defensible in an audit |
Merged match
When the same person or organization is on several lists you get one match: its score is the highest of the lists, and every list the subject is on is in lists. You do not get three rows for OFAC, UN and EU on the same person; you get one row with three lists.
Two records on the same list are separate matches even when their names are identical: two records a publisher lists under the same name are two different people or organizations.
Coverage status
coverageStatus | Meaning | When there is no match |
|---|---|---|
COMPLETE | Every requested risk domain was screened | Clear |
PARTIAL | At least one requested risk domain has no list in the publication; those domains are in uncoveredRiskDomains | Clear only for the screened domains — not for uncoveredRiskDomains |
DEGRADED | The candidate search that finds differently spelled names was unavailable, or the candidate window filled up on a very common name; similarly spelled names may have been missed | Do not treat it as a full screening; run it again |
Replaying a screening made before the switch to list publications with the same idempotency key returns publicationId, publicationRevision, coverageStatus and evidenceRef as null.
An empty datasetFreshnessJson means the freshness manifest could not be produced. Word your compliance statement conservatively — you cannot claim a specific list vintage.
When screening cannot run
If no active list publication can be pinned, the screening is not run and there is no fallback to older data. The response carries status: "failure" and HTTP 503:
| Endpoint | code |
|---|---|
| Name screening | sanction.screen_name.failed |
| Wallet screening | sanction.wallet.failed |
| Transaction screening | sanction.transaction.failed |
| Public procurement ban screening | sanction.public_procurement.failed |
A failed screening must never be read as "no match". Hold the customer record or transaction as "not screened", then retry the screening or send it to review. An empty matches list only means something when status is "success".
Wallet screening
POST /api/v1/sanction/screening/wallet| Field | Type | Required | Notes |
|---|---|---|---|
walletAddress | string | ✅ | The address to screen |
chain | string | — | Network (e.g. ethereum, bitcoin, tron, solana) |
mode | string | — | wallet_sanctions (default) | wallet_risk | wallet_full_aml — the wider modes add exposure and crypto-intelligence hits |
customerReference | string | — | Your own record id |
The address is looked up by exact match against the crypto wallet addresses carried by the lists in the active publication; there is no similarity search. Letter case is ignored only where it carries no information:
| Address format | Comparison |
|---|---|
EVM hex (0x…) | Case-insensitive |
Prefixed bech32 (bc1…, ltc1… etc.) | Case-insensitive |
| Prefixed CashAddr | Case-insensitive |
| Others (e.g. Bitcoin legacy, Tron, Solana) | Case-sensitive — send the address exactly as given |
Matching is by address, not by network label: the same EVM address is the same address on every EVM network. The network the list states is returned separately in listedChains.
{
"status": "success",
"code": "sanction.wallet.completed",
"data": {
"address": "0x…",
"chain": "ethereum",
"isSanctioned": true,
"riskScore": 100,
"riskLevel": "…",
"publicationId": "…",
"publicationRevision": 34,
"coverageStatus": "COMPLETE",
"evidenceRef": "…",
"publicationHits": [
{
"hitId": "…",
"entityId": "…",
"entityName": "…",
"entityType": "ORGANIZATION",
"hitType": "exact",
"score": 100,
"riskCategory": "sanctions",
"riskDomains": ["sanctions"],
"sourceListName": "OFAC SDN",
"lists": [ { "datasetCode": "OFAC", "displayName": "OFAC SDN", "…": "…" } ],
"listedAddress": "0x…",
"listedChains": ["ETH"],
"caseRule": "EVM_HEX"
}
],
"hits": []
},
"statusCode": 200
}| Field | Meaning |
|---|---|
publicationHits | Listed parties in the publication that hold the address. A party on several lists is one hit, with all its lists in lists |
riskCategory | The party's strictest risk domain |
listedAddress · listedChains | The address as the list writes it, and the network or currency the list states |
caseRule | The comparison rule applied: EVM_HEX | BECH32 | CASHADDR | CASE_SENSITIVE |
hits | Your own wallet list, exposure records and crypto intelligence. List-publication hits are never in this field |
isSanctioned | true when a party in the sanctions domain holds the address |
When a party in the sanctions domain holds the address, isSanctioned is true and the score is 100. A party in any other risk domain (e.g. wanted, regulatory) comes back with score 90 and is a hit that needs review. If no list publication can be pinned, wallet screening does not run either (503, above).
Account screening
POST /api/v1/sanction/screening/accountSanctions lists do not publish IBANs or bank account numbers. Account screening therefore screens your own account list (blacklist) and account risk data, not the list publication; its response has no publication fields. Screen the account holder's name separately with name screening.
Transaction screening
POST /api/v1/sanction/screening/transactionAll parties of a transaction are screened against the same list publication. The response adds publicationId, publicationRevision and coverageStatus; coverageStatus is the weakest coverage among the parties. Per-party evidence, coverage and list detail are in resultsJson. When no party was screened these fields are empty.
Public procurement ban screening
POST /api/v1/sanction/intelligence/public-procurement-banScreens a person or organization against the debarment (debarment) lists of the active publication, by name and Turkish TCKN/VKN.
| Field | Type | Required | Notes |
|---|---|---|---|
entityName | string | — | Person or organization name |
tckn · vkn | string | — | Matched exactly; an equal TCKN/VKN produces a match even when the name differs |
entityType | string | — | Person or organization |
onlyActive | boolean | — | Only bans currently in force |
limit | number | — | Maximum number of hits |
customerReference | string | — | Your own record id |
productChannel | string | — | Which product the call came from (reporting) |
At least one of entityName, tckn and vkn is required; with none of them the request is refused with sanction.screening.subject_invalid (400).
{
"status": "success",
"code": "sanction.public_procurement.completed",
"data": {
"matched": false,
"riskScore": 0,
"decision": "…",
"hits": [],
"publicationId": "…",
"publicationRevision": 34,
"coverageStatus": "PARTIAL",
"evidenceRef": "…",
"screenedRiskDomains": ["debarment"],
"searchedLists": [
{ "datasetCode": "…", "displayName": "…", "datasetVersionId": "…" }
],
"uncoveredSources": [
{ "datasetCode": "TR_PUBLIC_PROCUREMENT_BAN", "displayName": "…", "reason": "NOT_PUBLISHED" }
]
},
"statusCode": 200
}| Field | Meaning |
|---|---|
searchedLists | The debarment lists the screening read |
uncoveredSources | Sources expected to be screened but absent from the publication |
coverageStatus | PARTIAL while any source is uncovered |
hits[].matchBasis | What the match rests on (e.g. tckn, vkn, name) |
hits[].lists | The lists the hit appears on (same structure as name screening) |
Turkish public procurement bans (KİK) are not part of this screening. The list has no official machine-readable source, so it is not screened; the response reports TR_PUBLIC_PROCUREMENT_BAN in uncoveredSources with PARTIAL coverage. No match is not a clean result for public procurement bans in Türkiye — you need to check EKAP separately.
Screening history
GET /api/v1/sanction/screening/historyReturns earlier screenings. This answers "when and in which mode did we screen this customer" in an audit — fill customerReference and you will find it here.
Ongoing monitoring
A one-off screening does not warn you when the lists change. Register the subject for monitoring:
POST /api/v1/sanction/monitoring/register # register a subject
GET /api/v1/sanction/monitoring/alerts # new match alerts
GET /api/v1/sanction/monitoring/entities # monitored subjects
GET /api/v1/sanction/monitoring/matches # matches raised by monitoring
GET /api/v1/sanction/monitoring/dataset-freshness # dataset freshnessIf you screen at onboarding but do not register for monitoring, you will not hear about it when that customer is listed the next day. Onboarding screening and ongoing monitoring are two separate obligations.
Register body
{
"externalCustomerId": "cust_9001",
"subjectType": "person",
"profileJson": "{\"name\":\"Ahmet Yilmaz\",\"countryCode\":\"TR\",\"dateOfBirth\":\"1980-01-31\",\"identifiers\":[{\"type\":\"tckn\",\"value\":\"12345678901\"}]}",
"frequency": "DAILY",
"riskDomainScope": ["sanctions", "pep"]
}| Field | Meaning |
|---|---|
externalCustomerId | Your reference for the customer. It comes back in alerts and webhooks; registering the same reference again updates the profile. |
subjectType | person or organization. |
profileJson | The subject's profile as a JSON string: name (required), countryCode, dateOfBirth, identifiers[] (type, value). Re-screenings use this profile; identifiers sharpen the match. |
frequency | DAILY, WEEKLY or MONTHLY; daily when omitted. When the list publication changes, affected subjects are re-screened regardless of the frequency. |
riskDomainScope | Risk domains to monitor (sanctions, pep, rca, wanted, regulatory, debarment). The sanctions domain when omitted. |
Response: { "success": true, "monitoredEntityId": "<uuid>" }. The call works with an API key (sanction:monitoring scope) or a panel session; the Sanctions → Monitoring → Register for Monitoring card in the panel uses the same endpoint.
Monitoring webhooks
When a new match is found, a sanction.monitoring.match_found event is delivered to your endpoint; adverse media monitoring raises sanction.monitoring.adverse_media_found. The envelope, signature and retry rules are shared by all products — see Webhooks. Subscribe by selecting the event type on your webhook endpoint.
{
"type": "sanction.monitoring.match_found",
"data": {
"monitoredEntityId": "0b5d6a51-…",
"externalCustomerId": "cust_9001",
"subjectName": "Ahmet Yilmaz",
"matchedName": "Ahmet YILMAZ",
"sourceListName": "OFAC SDN",
"riskDomains": ["sanctions"],
"score": 93.4,
"canonicalEntityId": "d5990c41-…",
"datasetGeneration": "2026-10-19",
"detectedAt": "2026-10-19T06:12:40.000Z",
"caseId": null,
"screeningRequestId": "…",
"publicationId": "…",
"publicationRevision": 40,
"entityIds": ["d5990c41-…", "f89dd795-…"],
"lists": [{ "datasetCode": "OFAC", "displayName": "OFAC SDN" }]
}
}| Field | Meaning |
|---|---|
monitoredEntityId, externalCustomerId | Which monitoring record and which of your customers. |
subjectName, matchedName, score, riskDomains | The monitored profile, the listed name it matched, the score (0–100) and the risk domains. |
sourceListName, lists[] | The list presenting the match and every list the match appears on. |
canonicalEntityId, entityIds[] | The listed party and every record merged into the match; readable through the entity profile endpoint. |
publicationId, publicationRevision, datasetGeneration, detectedAt | Which list publication the match was found against, and when. Keep them for your audit trail. |
caseId | The case id when a case was opened automatically. |
One event is produced per match; a redelivery never produces a second notification. The same matches are listed by GET /api/v1/sanction/monitoring/matches.
Related endpoints
| Endpoint | What it does |
|---|---|
POST /api/v1/sanction/intelligence/adverse-media | Adverse media screening |
POST /api/v1/sanction/intelligence/public-procurement-ban | Public procurement debarment (above) |
GET /api/v1/sanction/intelligence/entity-search | Entity search |
GET /api/v1/sanction/intelligence/entity-profiles/:entityId | Entity profile |
GET /api/v1/sanction/intelligence/entity-graph/:entityId | Relationship graph |
GET /api/v1/sanction/intelligence/indirect-exposure/:entityId | Indirect exposure |
GET /api/v1/sanction/intelligence/ownership-aggregation/:entityId | Ownership aggregation (50% rule) |
POST /api/v1/sanction/blacklist | Adds an entry to your own blacklist. Your key must carry the sanction:add_list_entry scope; the entry is written to your list only |
GET /api/v1/sanction/blacklist | Lists your own blacklist (sanction:view_results) |