POST /certify/screening

Screens a file of counterparties against the sanctions lists, and keeps the proof: up to 200 names or crypto addresses in one call, each compared to the OFAC SDN and Consolidated lists, the EU financial sanctions list and the UN Security Council list. Every line comes back as a complete answer that names the lists it was compared with and the publication date of each, and the batch carries an Ed25519-signed Merkle root - so whoever receives your file can verify any single line offline, with the published verifier, without receiving the other lines and without calling us.

What is sold is the proof, not a better screening

The screening is exactly the one GET /screen/entity and GET /screen/address perform, called as is - same lists, same matching rules, same verdicts. A batch is N unit screenings and nothing else: there is no second matching logic, no extra list, no deeper search. If you only need the answer, the unit routes give you the same one.

What changes is the unit of sale. Each line becomes a fingerprinted record, and the tree of those fingerprints is signed. That turns “we screened our suppliers” into something a compliance officer, an auditor or a counterparty can check without trusting you or us: what was screened, against which lists, published on which dates.

What a no_hit does not certify

Every response carries this sentence in data.limits, verbatim:

This certifies, for each subject, the verdict returned by screening it against the designation lists named in the record, each carrying its own snapshot date, on the date of issue. A ‘no_hit’ or ‘not_listed’ verdict is NOT a certificate of non-membership: it is a dated statement that these lists, as published on those dates, contained no match under the matching rules described in the record. Homonymy is not resolved - a match is a name or an address that corresponds, never an identification of a person or of a wallet owner. Coverage is limited to the ingested lists (OFAC SDN, OFAC Consolidated, EU FSF, UN consolidated): national lists that are not ingested, politically exposed persons (PEP) and adverse media are not covered, and cryptocurrency addresses are matched only as the lists themselves publish them - no transaction analysis, no clustering, no derived addresses. No score, no rating and no recommendation to act is expressed or implied.

Four limits, each to be read on its own:

  1. A no_hit is not a certificate of non-membership. It is a dated statement: the lists named in the record, as published on the dates the record carries, contained no match under the stated rules. A list published the day after says nothing about it. This is the reading that breaks a “certified screening” - and it is wrong here.
  2. Homonymy is not resolved. A hit is a name that corresponds to a listed name, never the identification of a person. Two people can share a name; the record shows what matched (matched_name, match_type, the listed birth dates and identifiers) so a human can decide.
  3. Only the ingested lists. OFAC SDN, OFAC Consolidated, EU FSF and the UN consolidated list. National lists that are not ingested, politically exposed persons (PEP) and adverse media are not covered.
  4. Crypto addresses are matched only as the lists publish them. No transaction analysis, no clustering, no derived addresses. An address that received funds from a designated wallet, or that belongs to the same owner, is not_listed here unless a list publishes it verbatim. This is what a buyer coming from blockchain analytics assumes - and it is not what this route does.

And no score: the summary counts outcomes, it does not rank your file.

Vocabulary. What this produces is a signed attestation. It is not a certificate in the eIDAS sense - a regulated object with qualified timestamping. The JSON field is named certificate; the legal meaning is not implied.

Request

POST /certify/screening, Content-Type: application/json:

{
  "profile": "sanctions-screening",
  "include_weak": false,
  "subjects": [
    { "ref": "invoice-2211", "name": "Europäisch-Iranische Handelsbank", "kind": "entity", "country": "DE" },
    { "ref": "invoice-2212", "name": "Boulangerie Aurelia", "kind": "entity", "country": "FR" },
    { "ref": "invoice-2213", "address": "0x0330070FD38Ec3bB94F58FA55D40368271E9e54A" },
    { "ref": "invoice-2214", "address": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee" }
  ]
}
FieldRequiredDescription
profileyessanctions-screening - the only value. The body can only confirm the path, never change what is checked
subjectsyes1 to 200 lines - up to 200 subjects in one call
include_weaknoWhether weak name matches count towards the verdict (default false), as on /screen/entity - a policy for the whole batch, attested on every line
subjects[].refyesYour own reference for the line (an invoice, a supplier id), up to 128 characters - it is attested
subjects[].nameone ofA person or entity name, screened as /screen/entity screens it
subjects[].addressone ofA crypto address, screened as /screen/address screens it
subjects[].kind, .country, .birth_yearnoQualify a name - same values and validation as /screen/entity
subjects[].chainnoQualifies an address - same values as /screen/address

Each line carries exactly one of name or address.

Input order is strictly preserved - in records, and therefore in the leaves of the Merkle tree. That is what makes a line findable in the proof: its position is part of what is attested, next to your ref.

Unknown fields are refused, on the body and on every line. On a certified batch, a misspelt field silently ignored (adress for address) would become a billed line instead of a typo you can fix. A qualifier set on the wrong key (chain on a name) is refused for the same reason.

What is attested on each line: your ref and the line’s index, the key screened, the filters applied (kind, country, birth_year, include_weak - a name screened under country=RU has not undergone the same check as the same name without it), the verdict, the matches, and the date of every list.

200 response - UnifiedResponse

data holds profile, summary, records, certificate and limits; provenance is the sanctions store’s (snapshot, with the store’s as_of).

FieldTypeDescription
summaryobjectCounts, never a grade: subjects, by_issue (certified, unresolved), by_verdict (hit, weak_hit, no_hit, sanctioned, not_listed) - every key served, even at zero
recordsobject[]One per subject, in request order: { issue, data: { subject, verdict, verdict_reasons, blocks }, provenance, fingerprint }
certificateobjectThe attestation over the batch - see below
limitsstringThe fixed sentence quoted above

Each record is an answer on its own: extract it, attach the batch’s certificate, and it verifies offline.

  • issue is certified (a verdict was rendered) or unresolved (a well-formed key with nothing to compare it against - an address form no screened family recognizes and no list publishes). An unresolved line is served and billed: “no list publishes this address” is the answer bought.
  • verdict is hit, weak_hit or no_hit for a name; sanctioned or not_listed for an address; null only on unresolved. The field is always present.
  • blocks.lists_screened names every list compared, its volume and its own publication date (as_of). That is the point of the document: the regulatory question is not “is it listed?” but “what did you compare it with, and when?”. The four lists do not share a date, and the record never pretends they do.

A real batch of four lines

The request above, captured on 2026-10-15 - one entity hit, one name with no hit, one designated address, one address not listed:

{
  "data": {
    "profile": "sanctions-screening",
    "summary": {
      "subjects": 4,
      "by_issue": {"certified": 4, "unresolved": 0},
      "by_verdict": {"hit": 1, "no_hit": 1, "not_listed": 1, "sanctioned": 1, "weak_hit": 0}
    },
    "records": [
      {
        "issue": "certified",
        "data": {
          "subject": {
            "index": 0,
            "ref": "invoice-2211",
            "key": "name",
            "value": "Europäisch-Iranische Handelsbank",
            "filters": {"kind": "entity", "country": "DE", "include_weak": false}
          },
          "verdict": "hit",
          "verdict_reasons": [
            {"code": "name_match_exact"}
          ],
          "blocks": {
            "screening": {
              "query": {"name": "Europäisch-Iranische Handelsbank", "normalized": "europaisch iranische handelsbank"},
              "verdict": "hit",
              "verdict_basis": ["exact"],
              "hits": [
                {
                  "id": "EU:EU.13742.32",
                  "list_source": "EU-FSF",
                  "kind": "entity",
                  "matched_name": "Europäisch-Iranische Handelsbank",
                  "match_type": "exact",
                  "programs": ["IRN"],
                  "listed_on": "2025-09-29",
                  "birth_dates": [],
                  "nationalities": [],
                  "identifications": [],
                  "remarks": null
                },
                {
                  "id": "OFAC-SDN:25684",
                  "list_source": "OFAC-SDN",
                  "kind": "entity",
                  "matched_name": "EUROPAISCH-IRANISCHE HANDELSBANK AG",
                  "match_type": "exact",
                  "programs": ["IFSR", "IRAN", "NPWMD"],
                  "listed_on": "2018-11-05",
                  "birth_dates": [],
                  "nationalities": [],
                  "identifications": [
                    {"type": "Registration Number", "number": "HRB 14604"},
                    {"type": "SWIFT/BIC", "number": "EIHBDEHH"},
                    {"type": "SWIFT/BIC", "number": "EIHBIRTH"},
                    {"type": "Website", "number": "www.eihbank.de"},
                    {"type": "Additional Sanctions Information -", "number": "Subject to Secondary Sanctions"}
                  ],
                  "remarks": "All Offices Worldwide; (Linked To: BANK OF INDUSTRY AND MINE)"
                }
              ],
              "total_matches": 2,
              "truncated": false
            },
            "lists_screened": [
              {"list": "EU-FSF", "entities": 6241, "as_of": "2026-09-22"},
              {"list": "OFAC-CONS", "entities": 481, "as_of": "2026-09-14"},
              {"list": "OFAC-SDN", "entities": 19416, "as_of": "2026-10-09"},
              {"list": "UN", "entities": 1010, "as_of": "2026-10-10"}
            ]
          }
        },
        "provenance": {
          "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc",
          "fetched_at": "2026-10-15T15:49:28.725300583Z",
          "freshness": {"kind": "snapshot", "as_of": "2026-09-14T00:00:00Z"}
        },
        "fingerprint": "sha256:aCT2MxbsfYZR7fKL95brlCmgnoOdpvVfabrb_k3n84I"
      },
      {
        "issue": "certified",
        "data": {
          "subject": {
            "index": 1,
            "ref": "invoice-2212",
            "key": "name",
            "value": "Boulangerie Aurelia",
            "filters": {"kind": "entity", "country": "FR", "include_weak": false}
          },
          "verdict": "no_hit",
          "verdict_reasons": [],
          "blocks": {
            "screening": {
              "query": {"name": "Boulangerie Aurelia", "normalized": "boulangerie aurelia"},
              "verdict": "no_hit",
              "verdict_basis": [],
              "hits": [],
              "total_matches": 0,
              "truncated": false
            },
            "lists_screened": [
              {"list": "EU-FSF", "entities": 6241, "as_of": "2026-09-22"},
              {"list": "OFAC-CONS", "entities": 481, "as_of": "2026-09-14"},
              {"list": "OFAC-SDN", "entities": 19416, "as_of": "2026-10-09"},
              {"list": "UN", "entities": 1010, "as_of": "2026-10-10"}
            ]
          }
        },
        "provenance": {
          "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc",
          "fetched_at": "2026-10-15T15:49:28.725300583Z",
          "freshness": {"kind": "snapshot", "as_of": "2026-09-14T00:00:00Z"}
        },
        "fingerprint": "sha256:1iuHJEnUX-J7dDkeKcSZAZ38Ovs1QNVhUg4sO1C6_80"
      },
      {
        "issue": "certified",
        "data": {
          "subject": {"index": 2, "ref": "invoice-2213", "key": "address", "value": "0x0330070FD38Ec3bB94F58FA55D40368271E9e54A"},
          "verdict": "sanctioned",
          "verdict_reasons": [
            {"code": "address_designated", "detail": "OFAC-SDN"}
          ],
          "blocks": {
            "screening": {
              "query": {
                "address": "0x0330070FD38Ec3bB94F58FA55D40368271E9e54A",
                "address_normalized": "0x0330070fd38ec3bb94f58fa55d40368271e9e54a",
                "chain_family": "evm",
                "chain_family_source": "inferred"
              },
              "verdict": "sanctioned",
              "hits": [
                {
                  "entity_id": "OFAC-SDN:57056",
                  "entity_name": "AMNOKGANG TECHNOLOGY DEVELOPMENT COMPANY",
                  "list_source": "OFAC-SDN",
                  "programs": ["DPRK4"],
                  "listed_on": "2026-03-12",
                  "chain": "ETH",
                  "address_published": "0x0330070FD38Ec3bB94F58FA55D40368271E9e54A"
                }
              ]
            },
            "lists_screened": [
              {"list": "EU-FSF", "addresses": 7, "as_of": "2026-09-22"},
              {"list": "OFAC-CONS", "addresses": 0, "as_of": "2026-09-14"},
              {"list": "OFAC-SDN", "addresses": 1064, "as_of": "2026-10-09"},
              {"list": "UN", "addresses": 0, "as_of": "2026-10-10"}
            ]
          }
        },
        "provenance": {
          "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc",
          "fetched_at": "2026-10-15T15:49:28.725300583Z",
          "freshness": {"kind": "snapshot", "as_of": "2026-09-14T00:00:00Z"}
        },
        "fingerprint": "sha256:nGyVn1JtNB5817c-no5e58E3DDYDnxuL5EnnHunecnQ"
      },
      {
        "issue": "certified",
        "data": {
          "subject": {"index": 3, "ref": "invoice-2214", "key": "address", "value": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee"},
          "verdict": "not_listed",
          "verdict_reasons": [],
          "blocks": {
            "screening": {
              "query": {
                "address": "0x4465825B0ac713aA2Fa07Ea29023020804A128ee",
                "address_normalized": "0x4465825b0ac713aa2fa07ea29023020804a128ee",
                "chain_family": "evm",
                "chain_family_source": "inferred"
              },
              "verdict": "not_listed",
              "hits": []
            },
            "lists_screened": [
              {"list": "EU-FSF", "addresses": 7, "as_of": "2026-09-22"},
              {"list": "OFAC-CONS", "addresses": 0, "as_of": "2026-09-14"},
              {"list": "OFAC-SDN", "addresses": 1064, "as_of": "2026-10-09"},
              {"list": "UN", "addresses": 0, "as_of": "2026-10-10"}
            ]
          }
        },
        "provenance": {
          "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc",
          "fetched_at": "2026-10-15T15:49:28.725300583Z",
          "freshness": {"kind": "snapshot", "as_of": "2026-09-14T00:00:00Z"}
        },
        "fingerprint": "sha256:evRuAYBy6GcmzENmQFQC0OjfngEcHdS5G1dmjEIJWC4"
      }
    ],
    "certificate": {
      "certificate_version": 1,
      "profile": "sanctions-screening",
      "payload_hash": "sha256:6s3qZzCIzTolJ0Bo9au5gdPylWgG4DWj-HI6-j-ScoQ",
      "subject_count": 4,
      "issued_at": "2026-10-15T15:49:28.725300583Z"
    },
    "limits": "This certifies, for each subject, the verdict returned by screening it against the designation lists named in the record, each carrying its own snapshot date, on the date of issue. A 'no_hit' or 'not_listed' verdict is NOT a certificate of non-membership: it is a dated statement that these lists, as published on those dates, contained no match under the matching rules described in the record. Homonymy is not resolved - a match is a name or an address that corresponds, never an identification of a person or of a wallet owner. Coverage is limited to the ingested lists (OFAC SDN, OFAC Consolidated, EU FSF, UN consolidated): national lists that are not ingested, politically exposed persons (PEP) and adverse media are not covered, and cryptocurrency addresses are matched only as the lists themselves publish them - no transaction analysis, no clustering, no derived addresses. No score, no rating and no recommendation to act is expressed or implied."
  },
  "provenance": {
    "source": "ofac-sdn+ofac-consolidated+eu-fsf+un-sc",
    "fetched_at": "2026-10-15T15:49:28.725300583Z",
    "freshness": {"kind": "snapshot", "as_of": "2026-09-14T00:00:00Z"}
  }
}

Line by line:

  • invoice-2211 is a hit: the EU list publishes the name exactly, and OFAC SDN publishes EUROPAISCH-IRANISCHE HANDELSBANK AG with its registration number and BIC - which is what lets a human confirm the match is the bank and not a homonym;
  • invoice-2212 is a no_hit - as of the four dates in its lists_screened, and nothing more;
  • invoice-2213 is sanctioned: OFAC SDN publishes this exact address;
  • invoice-2214 is not_listed - no list publishes it verbatim, which says nothing about where its funds came from.

The certificate above is the one the service emits. Through the gateway, the same object also carries alg, key_id and signature - the service never signs anything, the gateway signs the root and nothing else.

An unresolved record

A Stellar account, whose form none of the screened address families recognizes and no list publishes:

{
  "issue": "unresolved",
  "data": {
    "subject": { "index": 0, "ref": "u1", "key": "address", "value": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7" },
    "verdict": null,
    "verdict_reasons": [
      {
        "code": "address_not_in_snapshot",
        "detail": "the value matches no known chain family and no screened list publishes it: there is nothing to compare it against"
      }
    ],
    "blocks": {
      "screening": null,
      "lists_screened": [
        { "list": "EU-FSF", "addresses": 7, "as_of": "2026-09-22" },
        { "list": "OFAC-CONS", "addresses": 0, "as_of": "2026-09-14" },
        { "list": "OFAC-SDN", "addresses": 1064, "as_of": "2026-10-09" },
        { "list": "UN", "addresses": 0, "as_of": "2026-10-10" }
      ]
    }
  },
  "provenance": { "…": "…" },
  "fingerprint": "sha256:tSuNCYPch8sv8u8O6zr9Y-4YTT81fynqKfc-LksOxE0"
}

verdict: null, never a fabricated not_listed: the lists were read, but there was nothing to compare this value with.

The attestation and the offline check

The mechanism is the same as POST /certify/batch, deliberately: one attestation format, one verifier, for both certifying routes. The service computes payload_hash - the Merkle root of the record fingerprints (JCS canonicalisation, SHA-256) - and holds no secret; the gateway writes alg, key_id and signature (Ed25519) over that root, and publishes its keys at /.well-known/certificate-keys. The detail - what is inside the signed scope, what is outside it, why a compromised key cannot forge a past verdict - is on the certify-batch page.

The verifier is the one already published: verify_certificate.py, Python standard library only, no network, no dependency on us. There is no second verifier for this route, so there is no choice to make about which one to trust.

curl -s https://api.invoket.com/.well-known/certificate-keys > keys.json
python3 verify_certificate.py batch.json --key keys.json
python3 verify_certificate.py record.json --key keys.json --proof proof.json

Real output on the batch above, as the service emitted it (the script’s messages are in French; the glosses are ours):

4 enregistrements, racine sha256:6s3qZzCIzTolJ0Bo9au5gdPylWgG4DWj-HI6-j-ScoQ ✓
issues : certified=4
verdicts : hit=1, no_hit=1, not_listed=1, sanctioned=1
profil « sanctions-screening » — la preuve ne couvre que les blocs de ce profil, jamais un contrôle complet
verdicts rendus le 2026-10-15T15:49:28.725300583Z contre : ofac-sdn+ofac-consolidated+eu-fsf+un-sc@2026-09-14T00:00:00Z
⚠️ réponse NON signée : elle est cohérente, mais rien n'atteste qu'elle vient d'Invoket
OK

The four records rebuild the published root; the summary is recounted; the profile is named; the issue date and the store date are stated - and the last warning is the important one: this capture was taken on the service, before the gateway signs, so the script says it is coherent but not proven to come from us. On a response obtained through the gateway, run with --key and that line becomes the Ed25519 check. Checks 1 to 4 prove coherence; only the signature proves origin.

What the verification proves is that the line is the one that was issued, against the lists and dates it names. What it does not - cannot - prove is that the world has not changed since: a list published after issued_at is outside the attestation by construction.

Errors

Only requests the service cannot answer leave the 200 range - none of them are billed.

StatuscodeCase
400INVALID_BODYMalformed JSON, a profile other than sanctions-screening, an unknown field on the body or on a line
400EMPTY_BATCHsubjects is empty
400BATCH_TOO_LARGEMore than 200 subjects
400INVALID_SUBJECTSAt least one line has a malformed key - the whole batch is refused, see below
503DATA_UNAVAILABLEThe sanctions store is not mounted, or does not carry a date for every list
500INTERNALInternal error (detail logged, not exposed)

One bad line refuses the whole batch. The gateway quotes on the subjects submitted, so serving a trimmed batch would bill a line that was never processed - and “half certified” is worse than not certified. The 400 then carries a rejected array naming every faulty line, so a batch of 200 is fixed in one pass rather than by bisection:

{
  "code": "INVALID_SUBJECTS",
  "error": "2/3 subjects were rejected: none of them was certified",
  "rejected": [
    { "code": "UNRECOGNIZED_ADDRESS_FORMAT", "index": 1, "reason": "parameter 'address' is not a recognizable cryptocurrency address", "ref": "b" },
    { "code": "INVALID_INPUT", "index": 2, "reason": "'chain' qualifies 'address', not 'name'", "ref": "c" }
  ]
}

No refusal echoes the content received; ref is omitted when ref itself is the faulty field.

A list without a date stops the certification, never a fallback date: a date invented in an attestation is worse than no attestation. The unit screening routes keep answering in that case - only certification stops.

Attribution

Designation data comes from the OFAC SDN and Consolidated lists (US Treasury, public domain), the EU financial sanctions list (European Commission) and the UN Security Council consolidated list, each as published on the date its record states.

See also