# API reference — OFAC Sanctions Screening: UN, EU, UK Lists + LEI & EU VAT Check

## One real dataset item

Mahan Air, local test run 2026-10-23, AI off (`rich_minds/counterparty-check-ai`):

```json
{
  "itemId": "bf6a796e3d9c",
  "name": "Mahan Air",
  "url": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=12927#bf6a796e3d9c",
  "input": {
    "name": "Mahan Air",
    "country": "IR"
  },
  "country": "IR",
  "verdict": "block",
  "verdictReason": "Block: likely listed — EU EU-EU.12240.1 'Mahan Air' (EU Iran regime (IRN); likely on name, country); OFAC_SDN OFAC_SDN-12927 'MAHAN AIR' (SDGT, NPWMD, IFSR; likely on name, country). Screening support from official lists, not legal advice — a block or review needs a human decision.",
  "sanctionsHits": [
    {
      "list": "EU",
      "entryId": "EU-EU.12240.1",
      "matchedName": "Mahan Air",
      "entryName": "Mahan Air",
      "entryType": "company",
      "score": 100,
      "weakAlias": false,
      "assessment": "likely",
      "decidingFields": [
        "name",
        "country"
      ],
      "reason": "name 100/100 and country IR agree",
      "adjudicatedBy": "rules",
      "programmes": [
        "IRN"
      ],
      "countries": [
        "IR"
      ],
      "listedOn": "2024-10-14",
      "sourceUrl": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32024R2697&qid=1728978549327",
      "subject": "counterparty",
      "foundVia": "name"
    },
    {
      "list": "OFAC_SDN",
      "entryId": "OFAC_SDN-12927",
      "matchedName": "MAHAN AIR",
      "entryName": "MAHAN AIR",
      "entryType": "company",
      "score": 100,
      "weakAlias": false,
      "assessment": "likely",
      "decidingFields": [
        "name",
        "country"
      ],
      "reason": "name 100/100 and country IR agree",
      "adjudicatedBy": "rules",
      "programmes": [
        "SDGT",
        "NPWMD",
        "IFSR"
      ],
      "countries": [
        "IR"
      ],
      "listedOn": null,
      "sourceUrl": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=12927",
      "subject": "counterparty",
      "foundVia": "name"
    },
    {
      "list": "US_CSL",
      "entryId": "US_CSL-65f7aaf1ecce3dcf5d990af266025b30384744ae6a13cfdeda6d7e30",
      "matchedName": "MAHAN AIR GENERAL TRADING LLC",
      "entryName": "MAHAN AIR GENERAL TRADING LLC",
      "entryType": "unknown",
      "score": 87,
      "weakAlias": false,
      "assessment": "unlikely",
      "decidingFields": [
        "country"
      ],
      "reason": "listed for AE, the counterparty is in IR",
      "adjudicatedBy": "rules",
      "programmes": [
        "DPL"
      ],
      "countries": [
        "AE"
      ],
      "listedOn": "2025-10-27",
      "sourceUrl": "https://www.bis.gov/licensing/end-user-guidance/denied-persons-list-dpl",
      "subject": "counterparty",
      "foundVia": "name"
    }
  ],
  "bestHitScore": 100,
  "hitCount": 3,
  "legalName": null,
  "lei": null,
  "leiStatus": null,
  "jurisdiction": null,
  "legalAddress": null,
  "registrationNumber": null,
  "leiChosenBy": null,
  "parents": [],
  "vatNumber": null,
  "vatValid": null,
  "vatName": null,
  "vatAddress": null,
  "vatNameMatches": null,
  "vatCheckedAt": null,
  "vatConsultationNumber": null,
  "vatError": null,
  "listsChecked": {
    "OFAC_CONS": "2026-10-21",
    "UK": "2026-10-16",
    "EU": "2026-10-15",
    "US_CSL": "2026-10-21",
    "UN": "2026-10-22",
    "OFAC_SDN": "2026-10-21"
  },
  "listsUnavailable": [],
  "changeType": null,
  "checkedAt": "2026-10-23T00:06:04+00:00",
  "isSample": false,
  "flags": [
    "likely_hit",
    "no_legal_entity"
  ],
  "targetFlagsMatched": [],
  "ruleScore": 3,
  "score": 3,
  "label": "hot",
  "fitScore": null,
  "scoreReasons": [],
  "summary": "",
  "generatedText": "Hold the deal: Mahan Air matches EU entry 'Mahan Air' under EU Iran regime (IRN) on name, country — compare the source entry before deciding.",
  "recommendedNextStep": "",
  "aiModel": null,
  "assessedBy": "rules",
  "chargedEvent": "qualified-counterparty-check-basic",
  "billedAs": "qualified-counterparty-check-basic",
  "dedupeKey": "cp:mahan-air|ir",
  "source": {
    "mode": "actor",
    "actorId": "own-source",
    "reader": "counterparty list (official lists, GLEIF, VIES read by this Actor)",
    "query": "Mahan Air"
  },
  "scrapedAt": "2026-10-23T00:06:04+00:00"
}
```

## Which call runs what — `OUTPUT.mode`

| Input shape | `OUTPUT.mode` | Can it charge? |
|---|---|---|
| `{}` or the untouched form | `demo` | no |
| `counterparties` typed (next to the sample too) | `live` | yes, per company |
| `sheetUrl` (a Google Sheet / CSV / .xlsx link), with or without `counterparties` | `live` | yes, per company |
| `sourceMode: "live"` (or its 0.1 name `"actor"`) + `counterparties` | `live` | yes, per company |
| the same with `autoSwitchToLive: false` | `demo` | no |
| `sourceMode: "list"` + your `itemsList` | `list` | yes, per company |
| `sourceMode: "dataset"` + `datasetId` | `dataset` | yes, per company |

## The `OUTPUT` record

`ok` and `outcome` (`ok` · `empty` · `source_refused` · `source_failed` · `error`) say how the run ended; branch on them,
never on an empty dataset. Keys of a successful live run (types as written; `null` when the run had nothing to say):

| Key | Type |
|---|---|
| `aiCoverage` | null |
| `aiError` | null |
| `aiMissed` | null — or `{wanted, assessed: 0, cause}` when the AI was on but assessed none of your companies |
| `aiTokens` | null |
| `aiTrial` | null |
| `alwaysDelivered` | array |
| `budgetLimitReached` | boolean |
| `candidates` | number |
| `chargeSplit` | null |
| `chargedEvents` | object |
| `dedupeStore` | null |
| `demo` | null |
| `demoIgnoredFields` | null |
| `demoRows` | null |
| `demoTextsDropped` | null |
| `demoTextsSearched` | null |
| `dedupeRoutedToRescreen` | null — or the re-screen store `dedupeAcrossRuns: true` turned on |
| `firstRunBackfill` | null |
| `freeAiReserve` | null |
| `freeTierRemaining` | null |
| `freeTierThisRun` | number |
| `history` | null |
| `historyDataset` | null |
| `listsChecked` | object |
| `listsUnavailable` | null |
| `mode` | string |
| `nextSteps` | array |
| `notDeliveredCap` | null |
| `ok` | boolean |
| `outcome` | string |
| `presetApplied` | null |
| `qualified` | number |
| `registrySkipped` | null |
| `reportUrl` | null |
| `rescreen` | null |
| `reviewAsked` | boolean |
| `sampleFallback` | null |
| `sanctionsFallback` | null |
| `scanCapHit` | null |
| `scheduleInput` | object |
| `scheduledSizing` | null |
| `seedsRolled` | null |
| `sourceError` | null |
| `sourceRefused` | null |
| `sourceStatus` | object |
| `stats` | object |
| `switchedToLive` | null |
| `targetFlags` | null |
| `timeSaved` | string |
| `timing` | object |
| `topProgrammes` | object |
| `trend` | null |
| `unchargedEvents` | null |
| `verdicts` | object |
| `webhook` | null |
| `yieldByQuery` | object |

## Flags (`targetFlags`)

`likely_hit`, `unclear_hit`, `parent_hit`, `weak_alias_only`, `no_hit`, `vat_invalid`, `vat_name_mismatch`, `vat_unchecked`, `no_legal_entity`, `lei_not_found`, `lei_lapsed`, `list_unavailable` (`likelyHit` works too). The flags come from the rules; how often a `likely_hit` is a true match is not yet measured on a labelled sample (roadmap 2026-11-06).

## API-only inputs

Declared in the input schema (the API tab, OpenAPI and MCP see them), hidden on the Console form:

| Input | Default | What it does |
|---|---|---|
| `slackWebhookUrl`, `discordWebhookUrl`, `teamsWebhookUrl`, `googleChatWebhookUrl`, `telegramUrl` | — | one alert URL per service — the form takes them all in **Send alerts to** |
| `webhookHeaders`, `webhookBatchSize`, `webhookSecret` | `{}`, `1`, — | extra headers, one POST per N rows, the HMAC signing secret |
| `dedupeAcrossRuns`, `dedupeTtlDays` | `false`, `0` | 0.1 names: `true` turns on the re-screen memory (`<dedupeStoreName>-rescreen` unless `rescreenStoreName` is set) — unchanged companies skipped, a new designation always delivered; never a skip for good |
| `dedupeStoreName` | `counterparty-check-ai-seen` | the store of the run history (trend, `OUTPUT.history`) |
| `aiCandidateMultiplier` | `2` | AI pool = this × `maxQualified`, riskiest first |
| `maxToProcess` | 3 × `maxQualified` | companies screened per run at most |
| `discoveryInput`, `discoveryTimeoutSecs` | `{}`, `3600` | input and time limit of the Store fallback (only when an official list is unreachable) |

## Which delivery field wins

Every URL is posted once; an explicit per-service field (API only: `slackWebhookUrl`, `teamsWebhookUrl`, `googleChatWebhookUrl`, `discordWebhookUrl`, `telegramUrl`) wins over the same service in **Send alerts to**; `webhookUrl` gets the rows as JSON, the chat services and `notifyEmail` get the digest.

## Webhook payloads

`webhookUrl` POSTs `{"event": "counterparty_check.qualified", "counterparty_check": {…}, "runId": "…"}` per company; with `webhookBatchSize` > 1 one POST per batch, `{"event": "counterparty_checks.qualified", "counterparty_checks": [{…}], "runId": "…"}`. A 429 / 5xx is retried once after 2 s; a failed POST never fails the run and is re-sent next run; `webhookSecret` signs every POST (`X-Signature: sha256=<HMAC-SHA256 of the raw body>`).

## Chain it after a supplier or buyer finder

Any Apify dataset of companies is input: `{"sourceMode": "dataset", "datasetId": "<the other run's dataset>", "rescreenStoreName": "suppliers-weekly"}`. The rows' `supplierName` (1688 Supplier Finder), `buyerName` (Alibaba RFQ Leads), `name` / `companyName` / `legalName` are the name; `country` / `buyerCountry` the country (a 1688.com supplier is CN); `vatNumber` / `vat` the VAT number. One supplier listed with ten products is one check. In the other Actor's **Integrations** tab add "Run succeeded → start another Actor" with this Actor and that input, `datasetId` set to `{{resource.defaultDatasetId}}` — every supplier it finds is screened in the same hour, and the re-screen memory charges each one once until its result changes.
