# OFAC SDN Sanctions Screening — Delisted History Check (`malekh/ofac-sdn-sanctions-screening-delisted-history`) Actor

OFAC SDN screening that tells FORMERLY\_SANCTIONED apart from NO\_MATCH. OFAC deletes a delisted party rather than flagging it, so the current SDN.XML answers both identically — this walks OFAC's publication-delta feed and returns the designation date, the removal date and the programmes.

- **URL**: https://apify.com/malekh/ofac-sdn-sanctions-screening-delisted-history.md
- **Developed by:** [Malek H](https://apify.com/malekh) (community)
- **Categories:** Automation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.01 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## OFAC SDN Sanctions Screening — with the delisting history

Screen names against the OFAC Specially Designated Nationals (SDN) list and get
an answer that distinguishes **"never sanctioned"** from **"no longer
sanctioned"**.

Standard OFAC screening answers one question: is this name on the SDN list
right now? For a party OFAC removed last month, the answer is *no match* — the
same empty result, byte for byte, as for a name that was never sanctioned in
the history of the programme.

That is not a shortcoming of the tools. It is how the source works.

### The mechanism

**OFAC does not flag a delisted party. It deletes it.** The current `SDN.XML`
export has no status field and no designation date anywhere in its schema —
each record carries a uid, names, aliases, programmes, addresses and identity
documents, and nothing at all about when the designation began or whether one
ever ended. A party removed from the list simply is not in the file, and the
file does not record that it ever was.

Measured 2026-08-06: **FLY BAGHDAD AIRLINES COMPANY** was designated
2024-01-22 under SDGT / Executive Order 13224 and removed in OFAC's 2026-08-05
publication. Streaming the entire 28.7 MB current `SDN.XML` finds
`FLY BAGHDAD` zero times and its a.k.a. `IRAQ EXPRESS` zero times. Nothing in
the export it was removed from records that it was ever on it.

The removal history lives on a different surface of the same host: OFAC
publishes every action as a separate delta file at `/changes/{publicationId}`.
There is no index endpoint and no bulk export — the ids are sequential in date
order back to the 2022-09-22 seed, roughly a third of the integers are
allocated, an unallocated one answers `400`, and the host rate-limits. Walking
the whole space is ~935 paced requests. This Actor does that walk **once**,
offline, ships the result inside its image, and at run time walks only
*forward* from the snapshot to OFAC's live publication — so results are current
to OFAC's latest action without every run re-walking four years of history.

### Verdicts

| Verdict | Meaning |
|---|---|
| `CURRENTLY_SANCTIONED` | On the SDN list in OFAC's current export |
| `SANCTIONED_NON_SDN_LIST` | Not on SDN, but on OFAC's Consolidated (non-SDN) list — **still restricted** |
| `FORMERLY_SANCTIONED` | Delisted — with `listedOn`, `removedOn` and the programmes it was under |
| `NO_MATCH` | No record on any of the three surfaces |

`SANCTIONED_NON_SDN_LIST` is here because **"removed from the SDN list" and "no
longer sanctioned" are not the same event.** Parties leave the SDN List *onto*
another OFAC list: OFAC's 2026-07-17 publication moved **39 parties** from the
SDN List to the Non-SDN Menu-Based Sanctions List while delisting only 9. Every
one of those 39 is absent from `SDN.XML` and every one is still restricted.

That distinction is also the trap in the delta feed, and it is why the
delisting counts here are what they are. `action="remove"` appears at two
levels in those files and means different things: on the `<entity>` element it
is a delisting, but on a `<sanctionsList>` element inside an otherwise
unchanged entity it is a move. Counting the string in that 2026-07-17 file
finds 165 "removals" where 9 parties were delisted. Worse, a move record
carries only the elements that changed — it has no names in it at all — so the
delta feed cannot even name the parties who moved. They are reached by name
from OFAC's Consolidated export instead, which this Actor screens as a third
surface.

### Point-in-time screening

Supply `asOfDate` (`YYYY-MM-DD`) and every row gains `sanctionedOnAsOfDate`,
answering the question a lookback audit actually asks: *was this counterparty
sanctioned on the day we transacted?*

It returns `true`, `false`, or **`null`** where the source genuinely cannot say
— a party still listed today whose designation predates OFAC's 2022-09-22 delta
seed has no designation date anywhere in the published data. That gap is
reported rather than filled with a guess, because an audit artefact with an
invented date in it is worse than one with an honest blank.

### Matching

Two deterministic rules, and nothing else:

1. **exact** — the normalised name matches a primary name or an a.k.a.
2. **all\_tokens** — every significant token of your query appears in the
   record's name (`RODRIGUEZ OREJUELA` matches
   `RODRIGUEZ OREJUELA, Gilberto Jose`). Corporate-form words — `LTD`, `GMBH`,
   `OOO`, `COMPANY` and so on — are ignored, and a single-token query never
   matches on the token rule, because one shared surname would match half the
   list.

Deliberately **not** fuzzy. A screening hit that a compliance officer cannot
reproduce by eye is not evidence, and an edit-distance threshold is a knob
whose false-positive rate nobody can state. Both rules above fit in one
sentence each, which is the bar for something that goes in an audit file.

Aliases are indexed alongside primary names. That matters: the delisted party
above is `FLY BAGHDAD AIRLINES COMPANY`, and the name on a shipping manifest is
`IRAQ EXPRESS`.

### Output

One row per name screened:

| Field | |
|---|---|
| `query` | the name you submitted |
| `verdict` | one of the four above |
| `matchedName` / `matchType` | the OFAC name that matched, and how (`exact` / `all_tokens`) |
| `entityType` | Individual / Entity / Vessel / Aircraft |
| `programs` | e.g. `SDGT`, `SDNTK`, `UKRAINE-EO13662` |
| `onList` | which OFAC surface the match came from |
| `listedOn` | designation date |
| `removedOn` | date OFAC removed it, on `FORMERLY_SANCTIONED` |
| `ofacUid` | OFAC's own record identifier |
| `remarks` | OFAC's free-text remarks on the record |
| `sanctionedOnAsOfDate` | point-in-time verdict when `asOfDate` is given |
| `matchCount` / `otherMatchedNames` | other OFAC records the query also hit |
| `sdnPublishDate` / `consolidatedPublishDate` / `historyThroughDate` | provenance for all three surfaces |

### It refuses rather than degrades

The failure this Actor has to refuse is specific: if the history is missing,
truncated or stale, every formerly-sanctioned party silently becomes
`NO_MATCH` — a confident false clear on a sanctions screen, which is the worst
possible output. So each of these **aborts the run with zero rows** instead of
degrading:

- the shipped history snapshot is missing or holds fewer than 500 delisted records
- OFAC's latest publication is more than 45 days old
- the forward walk cannot reach OFAC's live publication (id allocation changed)
- `SDN.XML` parses to fewer than 10,000 entries, or its own `Publish_Date` is
  more than 30 days old — freshness is read from the file's own date, never
  from an HTTP header
- `CONSOLIDATED.XML` parses to fewer than 100 entries, or is over 180 days old
- more than a quarter of delisted names also appear on the current SDN list,
  which would mean the two surfaces are describing different worlds

Data-load charges are applied **after** the sources are validated, so an
aborted run costs nothing.

### Source

Both surfaces are keyless, US federal public-domain data:

- `https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML`
- `https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/CONSOLIDATED.XML`
- `https://sanctionslistservice.ofac.treas.gov/changes/{publicationId}`

Requests are paced sequentially; this Actor is not going to be the reason a
free public service gets a rate limit.

### Not legal advice

This is a data tool, not a compliance determination. OFAC's published lists are
the authority, and a name match is a starting point for review — not a
conclusion about any person or entity.

# Actor input Schema

## `names` (type: `array`):

Individuals or entities to screen against OFAC, up to 2,000 per run. Each name returns one of four verdicts: CURRENTLY\_SANCTIONED, SANCTIONED\_NON\_SDN\_LIST (not on SDN but on OFAC's Consolidated list, so still restricted), FORMERLY\_SANCTIONED (with the date OFAC designated it, the date OFAC removed it and the programmes), or NO\_MATCH. Aliases are screened too — OFAC's a.k.a. records are indexed alongside primary names.

## `asOfDate` (type: `string`):

YYYY-MM-DD. Answers the lookback question a compliance audit actually asks: was this party sanctioned ON THIS DATE? Adds sanctionedOnAsOfDate to every row — true, false, or null where the source genuinely cannot say (a party still listed today whose designation predates OFAC's 2022-09-22 delta seed has no date anywhere in the source, and this reports that rather than guessing).

## `buildHistoryIndex` (type: `boolean`):

Maintenance only. Re-walks OFAC's publication-delta feed to regenerate the shipped history snapshot. Not needed for screening — the Actor brings its snapshot current on every run.

## `historyStartId` (type: `integer`):

Maintenance only.

## `historyEndId` (type: `integer`):

Maintenance only.

## Actor input object example

```json
{
  "names": [
    "FLY BAGHDAD AIRLINES COMPANY",
    "IRAQ EXPRESS",
    "AEROCARIBBEAN AIRLINES",
    "RODRIGUEZ OREJUELA, Gilberto Jose",
    "Acme Widgets Incorporated"
  ],
  "asOfDate": "2025-06-30",
  "buildHistoryIndex": false,
  "historyStartId": 1,
  "historyEndId": 1
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "names": [
        "FLY BAGHDAD AIRLINES COMPANY",
        "IRAQ EXPRESS",
        "AEROCARIBBEAN AIRLINES",
        "RODRIGUEZ OREJUELA, Gilberto Jose",
        "Acme Widgets Incorporated"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("malekh/ofac-sdn-sanctions-screening-delisted-history").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "names": [
        "FLY BAGHDAD AIRLINES COMPANY",
        "IRAQ EXPRESS",
        "AEROCARIBBEAN AIRLINES",
        "RODRIGUEZ OREJUELA, Gilberto Jose",
        "Acme Widgets Incorporated",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("malekh/ofac-sdn-sanctions-screening-delisted-history").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "names": [
    "FLY BAGHDAD AIRLINES COMPANY",
    "IRAQ EXPRESS",
    "AEROCARIBBEAN AIRLINES",
    "RODRIGUEZ OREJUELA, Gilberto Jose",
    "Acme Widgets Incorporated"
  ]
}' |
apify call malekh/ofac-sdn-sanctions-screening-delisted-history --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,malekh/ofac-sdn-sanctions-screening-delisted-history"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xoprhGMFfoTG30OKL/builds/G0UuRFBy2MroaUFUv/openapi.json
