An OFAC SDN check in 85 lines of Python, and the eight gaps it leaves open
OFAC publishes the SDN List as free machine-readable files with a dated delta for every change, so the basic automated check is an afternoon's work: we wrote one in 85 lines of Python and ran it on 4 September 2026 against the list published on 3 September. Everything hard sits outside the name match — aliases, transliteration, vessels, the 50 Percent Rule and the evidence you keep. Which of those you can live without is the real choice between building the check, buying an API and buying a suite.
- SDN.CSV primary names
- 19,326
- Rows in SDN.CSV published 3 September 2026: 9,926 entities, 7,518 individuals, 1,540 vessels, 342 aircraft.
- ALT.CSV alternate names
- 20,145
- 19,597 a.k.a., 530 f.k.a., 17 n.k.a. in the same publication.
- Delta files
- 1 per publication
- XML delta archive since 22 September 2022; ten publications between 30 July and 3 September 2026.
- Conservative threshold
- 0.85 token_sort_ratio
- 'Mohammed Ali' vs MUHAMMAD ALI scores 0.83 and is missed; 'Banco Exterior Cuba' vs BANCO EXTERIOR DE CUBA scores 0.93 and is caught (rapidfuzz 3.14).
- EU Financial Sanctions Files
- EU Login required
- webgate.ec.europa.eu/fsd/fsf redirects to EU Login; the Commission's documentation describes DELTA, GLOBAL and ANNUAL files.
Short answer
ANSWEROFAC gives you the files and a delta for every change, but no endpoint that
takes a name and returns a decision, so the matching is yours to write or to
buy. Writing it is Path A: download SDN.CSV and ALT.CSV from OFAC's
Sanctions List Service, normalise the names, run a conservative fuzzy match,
and log the file's Last-Modified header and hash as evidence; the 85-line
script below does exactly that, and ran on 4 September 2026 against the list
published on 3 September. Buying it is Path B, a screening API (one HTTP call
per check, candidates with scores, matched aliases and a stored evidence
record, no list maintenance), or Path C, an enterprise suite that adds case
management, payment screening and model governance for a review team. Path A
gives you a list hit and nothing else: no weak aliases, no transliteration,
no date-of-birth tie-breaks, no vessels, no 50 Percent Rule and no audit
trail, and the table further down says what closing each of those takes.
Path A, built on OFAC's own files
BUILDOFAC publishes the SDN List through the Sanctions List Service. The data
centre at sanctionslist.ofac.treas.gov listed these files on 4 September
2026, with the list last updated on 3 September:
| File | What it is | Size |
|---|---|---|
SDN.CSV, ADD.CSV, ALT.CSV, SDN_COMMENTS.CSV | Primary names, addresses, alternate names and remarks spill-over. No header row; null is written as -0-; linked by ent_num | 5.41 MB, 1.61 MB, 1.01 MB, 44 KB |
SDN.XML (SDN_XML.ZIP) | The same fields as the CSV set in one XML document | 27.63 MB (2.44 MB zipped) |
SDN_ADVANCED.XML (.ZIP) | Advanced data standard: typed identifiers, low-quality alias flags, scripts, relationships | 120.71 MB (5.42 MB zipped) |
SDN_ENHANCED.XML (.ZIP) | Enhanced data standard, a third XML schema | 103.96 MB (6.42 MB zipped) |
SDNLIST.PDF, SDNNEW26.PDF | Human-readable full list and the changes file | — |
YYYY-MM-DD_delta.xml | One delta file per publication since 22 September 2022: action="add" or action="remove" on entity records, add, remove or update on individual values (schema DeltaFile.xsd 1.0) | 11 KB (18 Aug) to 575 KB (24 Aug) |
XML.xsd, ADVANCED_XML.xsd, ENHANCED_XML.xsd, DAT_SPEC.TXT | Schemas and the CSV / fixed-width specification | — |
Every export sits under one base URL,
https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/,
followed by the file name. Delta files are served from
/api/download/delta?filename=DeltaArchive%2F2026-08-18_delta.xml.
The CSV layout comes from DAT_SPEC.TXT. SDN.CSV has twelve columns:
ent_num, SDN_Name, SDN_Type, Program, Title, five vessel fields,
Vess_owner and Remarks. SDN_Type is individual, vessel, aircraft
or -0- for an entity. ALT.CSV has five: ent_num, alt_num, alt_type
(aka, fka, nka), alt_name, alt_remarks. The 3 September publication
had 19,326 primary rows and 20,145 alternate names.
The script
Dependencies: Python 3.10 or later and rapidfuzz. Everything else is the
standard library.
#!/usr/bin/env python3
"""Minimal OFAC SDN check on OFAC's own CSV files.
Dependencies: Python 3.10+ and rapidfuzz (pip install rapidfuzz).
Usage:
python ofac_sdn_check.py "Banco Exterior de Cuba" "Wellbred Trading DMCC"
python ofac_sdn_check.py --sdn ofac-sdn-sample.csv "Wellbred Trading DMCC"
"""
import argparse, csv, hashlib, io, json, unicodedata, urllib.request
from datetime import datetime, timezone
from rapidfuzz import fuzz, process
SLS = ("https://sanctionslistservice.ofac.treas.gov"
"/api/PublicationPreview/exports/")
NULL = "-0-"
def load(name, local_path=None):
"""Return (text, evidence) for one OFAC file, local or downloaded."""
if local_path:
raw, stamp = open(local_path, "rb").read(), "local file"
else:
with urllib.request.urlopen(SLS + name, timeout=120) as resp:
raw, stamp = resp.read(), resp.headers.get("Last-Modified")
evidence = {"file": name, "last_modified": stamp,
"sha256": hashlib.sha256(raw).hexdigest()}
return raw.decode("utf-8", "replace"), evidence
def normalise(name):
text = unicodedata.normalize("NFKD", name)
text = "".join(c for c in text if not unicodedata.combining(c))
text = "".join(c if c.isalnum() else " " for c in text.upper())
return " ".join(text.split())
def build_index(sdn_text, alt_text):
records, index = {}, {}
for row in csv.reader(io.StringIO(sdn_text)):
if len(row) < 12:
continue
ent, name, kind, program = row[0], row[1], row[2].strip(), row[3]
records[ent] = {"sdn_name": name, "program": program,
"type": "entity" if kind == NULL else kind}
index.setdefault(normalise(name), []).append((ent, "primary"))
for row in csv.reader(io.StringIO(alt_text)):
if len(row) >= 4 and row[0] in records:
index.setdefault(normalise(row[3]), []).append((row[0], row[2]))
return records, index
def screen(query, records, index, threshold):
hits = process.extract(normalise(query), list(index), limit=10,
scorer=fuzz.token_sort_ratio,
score_cutoff=threshold * 100)
out = []
for matched, score, _ in hits:
for ent, via in index[matched]:
out.append({"score": round(score / 100, 2), "via": via,
"matched": matched, "ent_num": ent, **records[ent]})
return sorted(out, key=lambda h: -h["score"])
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("names", nargs="+")
ap.add_argument("--sdn", help="local SDN.CSV instead of a download")
ap.add_argument("--alt", help="local ALT.CSV instead of a download")
ap.add_argument("--threshold", type=float, default=0.85)
args = ap.parse_args()
sdn_text, sdn_ev = load("SDN.CSV", args.sdn)
if args.sdn and not args.alt: # sample run: no alias file
alt_text, alt_ev = "", {"file": "ALT.CSV", "note": "not loaded"}
else:
alt_text, alt_ev = load("ALT.CSV", args.alt)
records, index = build_index(sdn_text, alt_text)
run = {"screened_at": datetime.now(timezone.utc).isoformat("T", "seconds"),
"threshold": args.threshold, "list_evidence": [sdn_ev, alt_ev],
"primary_names": len(records), "names_indexed": len(index)}
print(json.dumps(run))
for name in args.names:
hits = screen(name, records, index, args.threshold)
print(json.dumps({"query": name, "candidates": hits}))
What a run looks like
We ran it on 4 September 2026 with a live download. The first line is the evidence record; the second is the result for one name added on 3 September. Download and match together took about seven seconds.
{"screened_at": "2026-09-04T12:53:11+00:00", "threshold": 0.85,
"list_evidence": [
{"file": "SDN.CSV", "last_modified": "Thu, 03 Sep 2026 15:37:44 GMT",
"sha256":
"447d09384cf840252569a30b5db51d8cc4aefb396c085168246ee04e7ba2693c"},
{"file": "ALT.CSV", "last_modified": "Thu, 03 Sep 2026 15:37:44 GMT",
"sha256":
"4511702ccd96b06ae8d5b55b1a9a12e938fc37a38da371e1285d1321b7a4aba6"}],
"primary_names": 19326, "names_indexed": 38985}
{"query": "Banco Exterior de Cuba", "candidates": [
{"score": 1.0, "via": "primary", "matched": "BANCO EXTERIOR DE CUBA",
"ent_num": "58490", "sdn_name": "BANCO EXTERIOR DE CUBA",
"program": "CUBA-EO14404", "type": "entity"}]}
To try it without the 7 MB download, use the
46-row sample (13 KB): rows copied
verbatim from SDN.CSV for the entries OFAC added between 18 August and
3 September 2026, plus three aircraft and three vessels. It deliberately has
no ALT.CSV. Run python ofac_sdn_check.py --sdn ofac-sdn-sample.csv "Wellbred Trading DMCC" and the best candidate is WELLBRED TRADING FZCO at
0.86, a near miss on the primary name. Add --alt ALT.CSV from the real
publication and the same query returns 1.00, because WELLBRED TRADING DMCC
is that entity's registered a.k.a. That gap is the subject of the next
section.
The choices the script makes for you
Normalisation strips accents, upper-cases and collapses punctuation, so
S.A. and SA agree and Al-Saghir becomes AL SAGHIR. The scorer is
token_sort_ratio, which ignores word order (Seye, Abdoulaye and
Abdoulaye Seye score 1.00) but not spelling. The cut-off of 0.85 is
conservative in one direction only: it keeps false positives down and lets
transliterations through.
| Query | Against | Score | At 0.85 |
|---|---|---|---|
| Banco Exterior Cuba | BANCO EXTERIOR DE CUBA | 0.93 | Candidate |
| Bluwaves Properties Ltd | BLUWAVES PROPERTIES LIMITED | 0.92 | Candidate |
| Zolghadr Mohammad Bagher | ZOLQADR, Mohammad Baqer (primary) | 0.87 | Candidate |
| Mohammed Ali | MUHAMMAD ALI | 0.83 | Missed |
| Al-Saghir Ruwaa | ALSAGHEER, Rawa (primary) | 0.48 | Missed; 1.00 via its a.k.a. AL-SAGHIR, Ruwaa in ALT.CSV |
What DIY misses
| Gap | Why the script misses it | What closing it takes |
|---|---|---|
| Aliases and weak a.k.a.s | ALT.CSV carries only the spellings OFAC wrote and no quality marker. The 3 September SDN_ADVANCED.XML flags 4,398 aliases LowQuality="true"; the CSV cannot tell them apart | Parse the advanced XML; write down whether a weak alias alone can raise an alert |
| Transliteration | Mohammed and Muhammad score 0.83; original-script names sit in the advanced XML as separate translations | Script-aware matching and transliteration tables, then a test set to prove they work |
| Date-of-birth tie-breaks | In the CSV, DOB is free text inside Remarks (DOB 10 Dec 1948; ...); the script never reads it | Parse typed features from the advanced XML; handle multiple and year-only dates; never auto-dismiss on one field |
| Vessels and aircraft | 1,540 vessels and 342 aircraft. The IMO number and the aircraft serial number sit in free text in Remarks (Vessel Registration Identification IMO 7406784); only call sign, type, tonnage and flag have columns, and names are reused and changed | Match on the identifier, not the name; the identifiers are typed in the advanced XML |
| The 50 Percent Rule | No list file names the entities blocked by ownership | Ownership data from registries or a KYB check, aggregated across every blocked owner |
| List-version evidence | The script logs Last-Modified and a SHA-256, but nothing keeps the file | Store every downloaded file and every result; process every delta in order |
| Audit trail | Output is JSON on stdout; nobody recorded who dismissed the 0.86 and why | A case store with dispositions, reviewer identity, timestamps and retention |
| Other lists | The Consolidated non-SDN list (CONS_* files), EU, UK and UN lists are separate downloads with separate semantics | One loader per list, with the list name on every hit |
None of this is an argument against Path A. It is the specification of what Path A is: a list hit on a primary name or a written alias, with the file version recorded. If that is the decision you need to defend, it is enough.
Path B, a screening API
INTEGRATEA screening API moves the list maintenance, the matching logic and the evidence store behind one endpoint. You send a name and whatever else you know, such as a date of birth, a country or an entity type, and you get back candidates with scores, the field or alias that produced each score, the list and programme, and a record you can retrieve later. The vendor pulls every publication and delta, parses the advanced XML, and carries the vessel identifiers and weak aliases you would otherwise parse yourself.
Re-screening is where this path earns its keep. A delta from OFAC is a list of changed records; the work is running your whole book against the new names, re-running every customer whose earlier candidates touched a changed record, and keeping the old result and the new one side by side. A provider that does this on every publication turns a weekly chore into a notification. Batch is the same argument at scale: a CSV of a thousand suppliers should come back as a thousand evidence records, not a thousand JSON lines.
The questions to ask are the ones the DIY table raises: whether the response
says which alias matched or only a score, whether it states the list
version, whether you can fetch the evidence a year later without it being
recalculated against the current list, whether vessels and the Consolidated
list are included, and what happens to the customers you already screened
when OFAC publishes a delta. ScreenVeritAI's REST API runs this pattern
(POST /api/v1/search returns a search id,
GET /api/v1/search/{search_id} returns the result) and stores each check
as a point-in-time PDF; the
OFAC screening API page has the request
and response shapes.
Path C, an enterprise suite
SCALEAn enterprise platform adds what neither a script nor a single endpoint provides: alert queues with four-eyes review and service levels, real-time screening of payment messages, model tuning with documented thresholds per list and per customer segment, and the model-risk paperwork a banking supervisor expects. It is bought by a review operation, not by a developer. The costs are the implementation project, the tuning cycle, and a per-seat or per-volume licence that rarely appears on a public price list. The supervisor's questions follow the platform in: how thresholds were set per list, how the test set was built, how often the model is re-validated, and who signed off the last tuning change. A suite gives you the tooling for those answers. It does not write them.
Where each path stops making sense
DECISIONVolume matters less than the question you have to answer afterwards. Our rule of thumb, stated as opinion:
| Situation | Path | Where it stops |
|---|---|---|
| Occasional checks, an internal question, no regulator asking for records | A | The first time someone asks "what did we check this name against, and who decided it was not a match" |
| Recurring checks on customers or suppliers, an obligation under sanctions or AML law, evidence needed per check | B | When screening has to sit inside a payment flow, or when a team of reviewers needs queues and service levels |
| Thousands of alerts a month, several reviewers, in-flight payment screening, supervisory model reviews | C | Only when that operation exists; before then the licence and the project cost buy capacity nobody uses |
Two boundaries hold whatever the volume. If the check has to be defended later, Path A needs a file store and a decision log bolted on, and at that point you have built a small version of Path B. And if you need the 50 Percent Rule, no path solves it from list files alone; the ownership data has to come from somewhere.
The EU's Financial Sanctions Files, behind a login
EUThe EU's consolidated list of persons, groups and entities subject to
financial sanctions is distributed through the Commission's Financial
Sanctions Files portal at webgate.ec.europa.eu/fsd/fsf. On 4 September
2026 the address redirected straight to EU Login with the message that the
service requires authentication. The Commission's FSF user manual confirms
that an FSF account is created through EU Login, and describes an "FSD
Files" area that lists each file with its checksum, a notification feed for
new publications, and tokenised URLs for crawlers in which the token is the
user's own identifier.
The documentation describes three file families. DELTA contains the
updates introduced by the most recently published regulation, GLOBAL is a
snapshot of all active records, and ANNUAL is the entire database with the
history of modifications and deletions. The files are posted, in principle,
on the day the Official Journal is published, in XML and CSV, and the
tokenised file names carry the schema version (_1_1).
We tested what we could without an account. A tokenised URL for the full
list returned a complete XML and CSV file (about 25 MB each) with no login,
which looks like good news until you read the file's own date: the XML's
generationDate was 5 August 2026 and its newest regulation was published
on 23 July, so the 21 and 27 August amendments to Regulation (EC) No
881/2002 were not in it. An anonymous URL is not evidence of a current list.
Use the token from your own account and record generationDate with every
run. We could not test the DELTA and ANNUAL files without an account, so
treat their endpoints as unverified here.
The XML itself is richer than OFAC's CSV set. Each nameAlias carries a
strong attribute, each birth date and citizenship is typed, and each record
cites the regulation and Official Journal page that listed it, so a hit can
be traced to its legal basis. And because the file is built from Council
regulations rather than from one list, the programme code on the record
matters as much as the name.
What to do next
ACTIONS- Decide which question the check must answer later: "was the name on the list" (Path A) or "what did we decide, against which version, and why" (B or C).
- If you build, keep every file you download, name it by
Last-Modified, and process every delta in order. The archive goes back to 22 September 2022. - Build a test set of true and false matches before choosing a threshold, and keep the set with the code.
- Write the alias policy down: weak a.k.a.s, transliterations, and the identifier used for vessels and aircraft.
- Add ownership data for the 50 Percent Rule, or record in the procedure that the check does not cover it.
- For EU obligations, request an FSF account now; the tokenised URLs and the notification feed are what an automated pull will use.
Frequently asked questions
Q&AWhere do I download the OFAC SDN list as a file?
From the Sanctions List Service data centre at sanctionslist.ofac.treas.gov. The files are SDN.CSV, ADD.CSV, ALT.CSV and SDN_COMMENTS.CSV (comma-delimited, no header row, null written as -0-), SDN.XML, SDN_ADVANCED.XML and SDN_ENHANCED.XML with their XSD schemas, plus fixed-width and PDF versions. The direct export URLs sit under sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/.
Does OFAC offer an API for screening names?
Not a screening endpoint. The Sanctions List Service exposes file exports under an /api/ path and OFAC runs the Sanctions List Search web tool, but nothing takes a name and returns a decision. The export URLs are stable enough to script, the HTTP response carries a Last-Modified header you can log, and the delta archive gives you one XML file per publication; the matching logic is yours to write or buy.
How often does the SDN list change?
Often enough that a weekly job is late. The delta archive lists ten publications between 30 July and 3 September 2026: 30 July, 5, 6, 7, 18, 20, 24, 26 and 28 August, and 3 September. The 24 August action alone produced a 575 KB delta file. Poll the Last-Modified header daily at least, and process every delta you missed in order.
What fuzzy-matching threshold should I use for an SDN check?
There is no universal number, only a documented trade-off. Our snippet uses token_sort_ratio at 0.85, which is conservative: it catches 'Banco Exterior Cuba' against BANCO EXTERIOR DE CUBA (0.93) but misses 'Mohammed Ali' against MUHAMMAD ALI (0.83). Build a test set of names you know are true and false matches, measure both error rates at several thresholds, and keep the test set under version control.
Does a name check cover OFAC's 50 Percent Rule?
No. Under OFAC's guidance of 13 August 2014 an entity owned 50 percent or more, directly or indirectly and in the aggregate, by blocked persons is blocked without appearing on the list. No SDN file carries that information. Closing the gap needs ownership data from registries or a KYB check, and a rule that aggregates the holdings of every blocked owner.
Do I also need OFAC's Consolidated (non-SDN) list?
Usually yes. The Consolidated Sanctions List files (CONS_PRIM.CSV, CONS_ALT.CSV, CONSOLIDATED.XML, CONS_ADVANCED.XML) carry the non-SDN programmes: Sectoral Sanctions Identifications, Foreign Sanctions Evaders, Non-SDN Menu-Based Sanctions, CAPTA, the Chinese Military-Industrial Complex list and the Palestinian Legislative Council list. Their restrictions differ from blocking, so the record needs to say which list produced the hit.
How is the EU list distributed compared with OFAC's?
Through the Financial Sanctions Files portal at webgate.ec.europa.eu/fsd/fsf, which requires an EU Login account; inside, you generate tokenised download URLs and subscribe to notifications. The Commission's documentation describes three file families: DELTA for changes since the last regulation, GLOBAL for a snapshot of active records and ANNUAL for the full history including deletions, in XML and CSV. On 4 September 2026 a public tokenised URL for the full list worked without signing in, but the file it returned had been generated on 5 August and lacked the August regulations. Use your own account's token and log the generationDate; we could not test the DELTA and ANNUAL files without an account.
Sources
SOURCES- 01Sanctions List Service
Office of Foreign Assets Control · 2026-09-04
- 02Specially Designated Nationals List — Data Center (file table)
Office of Foreign Assets Control, Sanctions List Site · 2026-09-04
- 03Archive of Published Delta Files
Office of Foreign Assets Control, Sanctions List Site · 2026-09-04
- 04Specially Designated Nationals and Blocked Persons — Data Specification (DAT_SPEC.TXT)
Office of Foreign Assets Control · 2026-09-04
- 05FAQ 401 — Entities Owned by Blocked Persons (50% Rule)
Office of Foreign Assets Control · 2026-09-04
- 06Financial Sanctions Files (FSF) — sign-in portal
European Commission · 2026-09-04
- 07FSF Stakeholders' User Manual
European Commission · 2026-09-04
- 08EU consolidated electronic list — processing the XML files (DELTA, GLOBAL, ANNUAL)
European Commission · 2026-09-04
Informational analysis of published regulatory sources. Not legal advice. Verify the primary sources before acting.