Sanctions screening is the one check in this certification where the main risk is not fraud. A disposable email costs you a trial account. A stolen card costs you a chargeback. Paying a sanctioned person or company can be a violation of law for your business, regardless of whether anyone intended harm, and regulators tend to ask a simple question afterwards: did you check, and can you show it?
That makes this lesson different in tone. The API call is the easy part. Most of the work is deciding what a fuzzy match means, who looks at it, what you record, and what you are actually obliged to do. Those obligations depend on where you operate, what you sell and who you pay, and this lesson cannot tell you what yours are. It can show you how to build screening that a compliance person would recognise as reasonable, and how to avoid the engineering mistakes that make it worthless.
01What a sanctions list is
Governments publish lists of people, companies, vessels and organisations that residents and businesses under their jurisdiction are restricted from dealing with. The best known is the US Treasury's OFAC Specially Designated Nationals (SDN) list. The UN Security Council, the EU and the UK each publish their own. They overlap but are not identical, and they change often, sometimes daily.
Each entry has a primary name, usually a set of aliases ("also known as"), and whatever identifying details the authority has: dates and places of birth, nationalities, addresses, passport numbers, and the sanctions programs it falls under. Those details matter, because the name alone is often not enough to tell two people apart.
02The call
Sanctions Screening (sanctionscreening) takes a name (at least three characters) and returns scored matches. It costs 20 credits per call, which is the most expensive check in this course, so you will want to be deliberate about when you run it.
curl "https://api.apiverve.com/v1/sanctionscreening?name=Banco+Nacional+de+Cuba" \
-H "x-api-key: YOUR_API_KEY"An exact hit on the primary name. verdict summarises the best match; results holds every candidate.
The top-level fields are the ones your code branches on:
| Field | Meaning |
|---|---|
matched | At least one candidate scored at or above the threshold |
matchCount | How many candidates came back |
verdict | Summary of the best match: exact, high, possible or none |
topScore | The best candidate's score, 0 to 1 |
matchedLists | Which lists produced the matches |
lists | Which lists were screened, with each list's publishDate, recordCount and lastUpdated |
Each item in results has name, type (individual, entity and so on), score, confidence, programs, source (the list code) and sourceId. Paid plans add the detail you need to resolve a match: matchedOn (the exact name or alias that matched), matchedVia (primary or aka), isWeakAlias, aka, dateOfBirth, nationalities, addresses and ids. The threshold, limit and type parameters are also paid-plan options. lists is available on every plan and takes a comma-separated set of codes: ofac_sdn, ofac_cons, un, eu, uk.
03Fuzzy matching is the point, and the problem
Exact matching would be useless. Names are transliterated from other alphabets in several ways, people drop or reorder parts of their names, and anyone trying to evade a screen will misspell on purpose. So screening is fuzzy by design. Try a misspelling:
curl "https://api.apiverve.com/v1/sanctionscreening?name=Banko+Nacional+de+Kuba" \
-H "x-api-key: YOUR_API_KEY"Two letters wrong, and the bank still matches with verdict high and a score around 0.93.
That is what you want. Now the cost of it. Screen a common name:
curl "https://api.apiverve.com/v1/sanctionscreening?name=Mohammed+Ali" \
-H "x-api-key: YOUR_API_KEY"Several different listed individuals match through their aliases, each with a high score.
At the default threshold of 0.85, Mohammed Ali returns several unrelated listed individuals, each scoring 0.96, each matched through an alias that contains both names. Millions of people share that name. Almost every one of them is not on any list. If your system blocks every customer who produces a high verdict, it will discriminate against people with common names from particular regions, and it will do it confidently.
This is the false positive problem, and it is the central fact of sanctions screening. The screen is a filter that hands candidates to a human. It does not make the decision.
Reading a match
When a screen returns a candidate, a reviewer asks whether the person in front of them is the person on the list. The useful evidence is everything other than the name:
- Type. An
entitymatch against an individual customer, or the reverse, is usually a false positive. - Date of birth. A listed person born in 1965 is not your 24-year-old customer.
- Nationality and addresses. Weak on their own, strong together.
- Which name matched. A hit via a weak alias (
isWeakAlias: true), such as a nickname or a single common word, is much less meaningful than a hit on the primary name.
This is why you collect date of birth and country at onboarding if you are going to screen individuals. Without them, a reviewer has nothing to resolve a match with, and every common name becomes a stuck case.
Screening a new individual customer returns verdict: high, topScore 0.96, matched through an alias of a listed individual whose recorded year of birth is 1965. Your customer's verified date of birth is 1999. What is the right handling?
04Choosing a threshold
On paid plans threshold runs from 0.5 to 1. Lowering it catches more misspellings and more false positives; raising it does the reverse. There is no universally correct value, but there are two wrong ways to pick one.
The first is raising it until the review queue goes quiet. If 0.95 stops the false positives on common names, it also stops the deliberate misspellings that screening exists to catch. Your queue is quieter because your screen is weaker.
The second is picking a number once and never revisiting it. Keep every reviewed case with its score and the reviewer's conclusion. After a few hundred, you can see where true matches actually score against false ones, and you can defend the threshold you chose with data.
A reasonable starting point is the default 0.85, with exact and high verdicts routed to review and possible logged for periodic sampling. Write down why you chose it.
05When to screen, and how often
Screening only at signup misses two things: people added to a list after they signed up, and details that change. Common practice is to screen at these points:
- Onboarding, before you activate an account that can receive money or move it.
- Before a payout, if payee details changed since the last screen.
- On a schedule, re-screening your active customers and payees as lists are updated. The
lists[].publishDatefield tells you when the list behind your last screen was published, so you can tell whether a newer one exists.
Screen the names you will actually transact with: the legal name of a business payee, not just the trading name, and the beneficial owners where you collect them. A company that is not itself listed can still be owned or controlled by someone who is, and some regimes treat that as equivalent.
06Supporting checks for payees
If you pay US businesses or contractors, you usually collect a taxpayer identification number alongside the name. Tax ID Validator (taxidvalidator) checks that an SSN, EIN or ITIN is structurally valid, for 10 credits. It does not confirm the number belongs to the name. That requires a match against the tax authority's records, which this API does not do.
curl "https://api.apiverve.com/v1/taxidvalidator?taxid=000-12-3456" \
-H "x-api-key: YOUR_API_KEY"valid is false and error gives the reason: an SSN area number can never be 000.
On every plan you get valid, normalized, masked, last4 and error. Paid plans add type (SSN, EIN or ITIN) and is_placeholder, which flags well-known dummy numbers like 123-45-6789 and 12-3456789 that are structurally valid but obviously fake. Store masked and last4 in your logs, never the full number: a TIN is sensitive personal data and your audit trail should not become a leak.
07An auditable screening function
Here is the screen as a module, ESM on Node 18+. The important part is not the call, it is the record it writes.
import { appendFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
const BASE = 'https://api.apiverve.com/v1';
async function verve(api, params) {
const res = await fetch(`${BASE}/${api}?${new URLSearchParams(params)}`, {
headers: { 'x-api-key': process.env.APIVERVE_API_KEY },
signal: AbortSignal.timeout(5000),
});
const body = await res.json().catch(() => null);
if (!res.ok || body?.status !== 'ok') throw new Error(body?.error || `HTTP ${res.status}`);
return body.data;
}
export async function screenParty({ partyId, name, kind, birthYear }) {
const screenedAt = new Date().toISOString();
let outcome, data;
try {
data = await verve('sanctionscreening', { name });
if (!data.matched) outcome = 'clear';
else outcome = 'review'; // a human resolves every candidate
} catch (e) {
outcome = 'pending'; // never treat a failed screen as clear
}
const record = {
screeningId: randomUUID(),
partyId,
kind,
nameScreened: name,
screenedAt,
outcome,
verdict: data?.verdict ?? null,
topScore: data?.topScore ?? null,
candidates: (data?.results ?? []).map((r) => ({
sourceId: r.sourceId, source: r.source, name: r.name, type: r.type,
score: r.score, programs: r.programs, dateOfBirth: r.dateOfBirth ?? null,
})),
listsScreened: (data?.lists ?? []).map((l) => ({ code: l.code, publishDate: l.publishDate })),
context: { birthYear: birthYear ?? null },
};
await appendFile('screenings.jsonl', JSON.stringify(record) + '\n');
return record;
}
if (process.argv[2]) {
console.log(await screenParty({ partyId: 'demo', name: process.argv[2], kind: 'individual' }));
}Run it with node screen.js "Banco Nacional de Cuba" and look at the line it appends.
import json, os, sys, uuid, datetime, requests
BASE = "https://api.apiverve.com/v1"
def verve(api, params):
r = requests.get(f"{BASE}/{api}", params=params,
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]}, timeout=5)
body = r.json()
if not r.ok or body.get("status") != "ok":
raise RuntimeError(body.get("error") or f"HTTP {r.status_code}")
return body["data"]
def screen_party(party_id, name, kind, birth_year=None):
screened_at = datetime.datetime.now(datetime.timezone.utc).isoformat()
data = None
try:
data = verve("sanctionscreening", {"name": name})
outcome = "review" if data["matched"] else "clear"
except Exception:
outcome = "pending"
record = {
"screeningId": str(uuid.uuid4()),
"partyId": party_id, "kind": kind, "nameScreened": name,
"screenedAt": screened_at, "outcome": outcome,
"verdict": (data or {}).get("verdict"),
"topScore": (data or {}).get("topScore"),
"candidates": [
{"sourceId": r.get("sourceId"), "source": r.get("source"), "name": r.get("name"),
"type": r.get("type"), "score": r.get("score"), "programs": r.get("programs")}
for r in (data or {}).get("results", [])
],
"listsScreened": [{"code": l["code"], "publishDate": l["publishDate"]}
for l in (data or {}).get("lists", [])],
"context": {"birthYear": birth_year},
}
with open("screenings.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(record) + "\n")
return record
if __name__ == "__main__" and len(sys.argv) > 1:
print(json.dumps(screen_party("demo", sys.argv[1], "individual"), indent=2))Three rules are encoded here.
Every match goes to review. The function never returns block. A human looks at the candidates, compares the details, and records a decision with a reason. That decision is a separate record linked by screeningId.
A failed screen is pending, not clear. In the signup gate from the Fundamentals level, an outage degraded to a lighter check. Here it must not. Hold the payout or the activation until a screen succeeds. A delay is an inconvenience; an unscreened payment to a listed party is a much bigger problem.
The record is append-only and self-contained. It says what name was screened, when, against which list versions, what came back and what you did. In production that goes in a database table with no update or delete permission for the application, not a local file. You may need to show it years later.
Your sanctions screen times out while onboarding a new payee. Which handling is appropriate?
08What screening does and does not satisfy
A name screen is one control. Depending on your business, the obligations around it can include knowing who your customers are, identifying the owners behind business customers, monitoring transactions, filing reports, and restricting business with sanctioned countries and regions as well as listed parties. Country-based restrictions are a reason to look at the IP and BIN signals from earlier lessons, but those signals are evidence for review, not proof of location.
What a well-built screen gives you is a defensible process: you checked the right names, at the right moments, against current lists, sent every candidate to a person, and kept a record. That is the part engineering owns. The rest is a conversation with whoever owns compliance in your business.
The last lesson pulls everything in this certification together: contact checks, network signals, card and bank checks, and sanctions results, combined into one score with reasons attached. Continue to Build an explainable risk score.