Sanctions News Desk · API

    Our screening API never returns a match score. Here are the three responses it does return

    A screening check comes back one of three ways: no listing, an exact hit, or a candidate that matches the name and nothing else. The third is the one that consumes your week. Our v1 API returns no similarity score for it, on purpose, and the three responses printed below are the argument for why the thing you configure is a review policy rather than a number.

    By ScreenVeritAI Team, Sanctions News Desk

    · Updated

    Key facts

    • Create a screening: POST /api/v1/search → 202
    • Authentication: X-API-Key header
    • Match score in the response: None — by design
    • The one numeric threshold: 0.95, to discard
    • Price per check: €0.39 (search_type sanctions_check)

    Short answer

    POST /api/v1/search with an X-API-Key header and a JSON body containing entity_name. It answers 202 with a search_id — never with a result, because one check runs across the whole source register. Poll GET /api/v1/search/{search_id} until status is completed, then read the per-source fields. Each of those fields is a short finding whose first line is a Status: Sanctioned, Potential PEP match, Potential watchlist match or No relevant listing found. There is no numeric score in the response and no threshold in the request. What you design instead is a review policy: which statuses enter your review queue, and who signs the disposition.

    The name is the only field that is matched

    curl -X POST https://screenveritai.com/api/v1/search \
      -H "X-API-Key: svai_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
            "entity_name": "Example Trading Ltd",
            "entity_type": "company",
            "search_type": "sanctions_check",
            "language": "en"
          }'
    
    FieldRequiredWhat it does
    entity_nameyesThe string that is actually matched. 1–500 characters.
    entity_typenoperson, company or any. Stored and echoed back; it does not narrow the list matching.
    search_typenoOne of sanctions_check, adverse_media, full_search, deep_research_report. Omitting it selects full_search — send it explicitly.
    countrynoJurisdiction hint. Used for identity resolution in deep_research_report; ignored by list matching.
    identifier, websitenoPublic registration number and site, for the report search types.
    languagenoReport language, e.g. en, pl.

    Read that table once and the design is plain: the name is the query. Everything else is metadata you will be glad to have in your own audit trail. There is one trap in that generosity: unknown JSON keys are ignored rather than rejected, so a misspelled field name fails silently. Validate the payload on your side.

    The answer is immediate and content-free:

    {
      "search_id": "3f7c2a51-9b4e-4c8a-8d21-6f0e4b7a55d3",
      "status": "processing",
      "message": "Search queued successfully. Poll the GET endpoint for results.",
      "estimated_time_seconds": 15
    }
    

    estimated_time_seconds is a fixed hint derived from the search type — 15 for sanctions_check, 20 for adverse_media, 30 for full_search, 1,800 for deep_research_report. It is a scheduling hint, not a measurement and not a service level. Use it to size your first sleep, not your timeout.

    Results arrive only by polling

    curl https://screenveritai.com/api/v1/search/3f7c2a51-9b4e-4c8a-8d21-6f0e4b7a55d3 \
      -H "X-API-Key: svai_your_key_here"
    
    statusMeaningClient action
    processingRunningKeep polling, with back-off
    queuedWaiting for the durable worker (report type only)Keep polling
    completedResults attachedRead and store
    error_streamingThe run failed mid-executionTerminal — log and retry as a new search
    error_no_final_payloadThe run produced no payloadTerminal
    error_saving_resultsThe run finished, the write failedTerminal — contact support with the id
    error_credit_deduction / error_queue_createBilling or queueing failed before work startedTerminal

    The whole client rule is one line: poll until status == "completed" or status.startswith("error_"). All five documented failure statuses begin with error_. A prefix test also catches any new failure status that follows the same naming; a hard-coded list of the five would not. Polling counts against your rate limit, so start at the ETA and back off.

    A no-hit response still returns a field for every source screened

    Trimmed to the fields that carry meaning; the real body repeats the same shape for every jurisdiction. The field names and status words in the three responses are the API's own. The names, references, dates and URLs are invented examples.

    {
      "id": "3f7c2a51-9b4e-4c8a-8d21-6f0e4b7a55d3",
      "entity_name": "Example Person 01",
      "entity_type": "person",
      "status": "completed",
      "search_type": "sanctions_check",
      "ofac_sanctions": "## OFAC Sanctions Findings\n\n**Status:** *No relevant listing found*\n\n**Details:**\n- No relevant listing was found in this source for the searched entity.\n\n**Source:** *OFAC Sanctions*",
      "eu_sanctions": "## EU Sanctions Findings\n\n**Status:** *No relevant listing found*\n\n...",
      "uk_sanctions": "...", "un_sanctions": "...",
      "criminal_watchlists": null,
      "pep": null,
      "sanctions_sources": [
        { "source_id": "ofac_sanctions", "title": "OFAC Sanctions", "authority": "Office of Foreign Assets Control", "details": "...", "hit_count": 0 },
        { "source_id": "eu_sanctions", "title": "EU Sanctions", "authority": "", "details": "...", "hit_count": 0 }
      ],
      "created_at": "2026-09-04T09:12:41.882431+00:00",
      "completed_at": "2026-09-04T09:12:57.114902+00:00"
    }
    

    There is one field per main jurisdiction — ofac_sanctions, eu_sanctions, uk_sanctions, un_sanctions, pl_sanctions, fr_sanctions, canada_sanctions, australia_sanctions, switzerland_sanctions, south_africa_sanctions and new_zealand_sanctions — and each one is a short Markdown finding written for a person to read. A source that was screened and found nothing still returns a field, and that distinction carries weight. Absence of a field means the source was not part of that run; a no-hit finding means it was.

    sanctions_sources is the machine-readable twin of those strings, and the one you should branch on: one object per screened source with a hit_count on each, covering the 11 main jurisdictions and the additional registers behind them. sum(hit_count) is your alarm; the 11 strings are for the human. additional_sanctions_sources carries the same objects for the non-jurisdiction registers only, and exists for older clients. criminal_watchlists and pep come back null when nothing matched — those two domains report findings and nothing else, so a null is the no-hit case and any non-empty string is something to read. completed_at, paired with created_at, is what you store as the evidence timestamp.

    Store this. Do not label it. "No relevant listing found in the checked sources on 4 September 2026" is a defensible sentence; "clear" is a claim about the world that a name lookup cannot support.

    An exact sanctions hit names the list, the reference and the URL

    {
      "entity_name": "Example Trading Ltd",
      "status": "completed",
      "search_type": "sanctions_check",
      "ofac_sanctions": "## OFAC Sanctions Findings\n\n**Status:** *Sanctioned*\n\n**Details:**\n- **Matched entity:** EXAMPLE TRADING LTD\n- **Identification:** 51234; EXAMPLE TRADING LIMITED; EXAMPLE TRD\n- **Justification:** Acting for or on behalf of a designated entity.\n- **Measures:** SDGT\n- **Dates:** 2026-08-26\n- **Authority:** Office of Foreign Assets Control\n- **Evidence URL:** https://sanctionssearch.ofac.treas.gov/Details.aspx?id=99999\n\n**Source:** *Office of Foreign Assets Control*",
      "sanctions_sources": [
        { "source_id": "ofac_sanctions", "title": "OFAC Sanctions", "authority": "Office of Foreign Assets Control", "details": "...", "hit_count": 1 }
      ]
    }
    

    Unescape that string and you get a short structured finding. Every line in it is an instruction:

    LineWhat you do with it
    Matched entityThe register's own spelling. Compare it to your contract spelling; a difference is the start of the review, not the end of it.
    IdentificationThe list's unique reference plus aliases. The reference is what you quote in the file — it survives spelling changes.
    JustificationThe authority's stated grounds, where the source publishes them.
    MeasuresProgramme or regime code (SDGT, asset freeze, travel ban). Determines which obligation bites.
    DatesListing or designation date. If it is later than your last screening, your last screening was not wrong — it was earlier.
    AuthorityThe issuing body. Decides who you notify and under whose rules.
    Evidence URLThe authority's own record. Put this link in the file; a vendor screenshot is not a source.

    An exact hit on a sanctions list is not a decision you take alone. It is a freeze-and-escalate event under your own procedure, and the evidence URL plus the identification reference are what your report to the competent authority will be built from.

    The ambiguous response matches the name and nothing else

    This is the response that consumes your week: the name matches, the identity does not resolve.

    {
      "entity_name": "A. Example",
      "status": "completed",
      "search_type": "sanctions_check",
      "ofac_sanctions": "## OFAC Sanctions Findings\n\n**Status:** *No relevant listing found*\n\n...",
      "pep": "## PEP Findings\n\n**Status:** *Potential PEP match*\n\n**Details:**\n- **Matched entity:** A. Example\n- **Identification:** Andrzej Example; 1971; pl\n- **Datasets:** Poland Public Officials Register\n\n**Source:** *Poland Public Officials Register*",
      "criminal_watchlists": null
    }
    

    Read the status words literally. A sanctions finding says Sanctioned — an assertion about a register. PEP and criminal-watchlist findings say Potential PEP match and Potential watchlist match — an assertion about a resemblance. That word "potential" is the API telling you, in the only vocabulary it has, that this is a candidate and not a conclusion.

    What you have here is an initial for a given name, a birth year rather than a date, and a country. What you do not have is a person. The next step is not a threshold; it is three cheap identity checks: whether the customer's recorded date of birth contradicts 1971, whether the register entry's role fits the customer's profile, and whether the dataset publishes an identifier your KYC file also holds. Two contradictions close it as a false positive. One agreement and one gap keep it open.

    There is no 0.82

    The retrieval layer does compute similarity internally. It is not returned, and that is deliberate: a number without the reasoning behind it is a decision you cannot defend and a control you cannot calibrate. Print a score and every review meeting turns into an argument about the score.

    There is exactly one numeric threshold in the pipeline, and it runs the other way. Full Search adds a review stage over the candidates, and a candidate is removed only when the review is at least 0.95 confident it is a false positive and can write the reason. Everything below that bar stays in front of you, reason attached.

    So the threshold you choose is a policy, not a parameter. The queues and targets below are our recommendation, not a rule from any regulator; set your own:

    Response containsQueueTarget
    Any hit_count > 0 in sanctions_sourcesFreeze and escalateSame day
    Potential watchlist matchSenior review1 business day
    Potential PEP matchStandard review, EDD if confirmed2 business days
    Everything No relevant listing foundAuto-file as evidenceNo human

    A result becomes evidence when an analyst writes the disposition

    A screening result is not an audit artefact until a human has written what it means. Store one disposition row per candidate, not per search:

    ColumnExample
    search_id3f7c2a51-…
    source_idofac_sanctions
    matched_entityEXAMPLE TRADING LTD
    list_reference51234
    dispositiontrue_match / false_positive / escalate
    rationale"Date of birth in file is 1984-02-11; register entry states 1971. Different person."
    decided_by, decided_atanalyst id, ISO timestamp

    The rationale field is the whole point. An examiner does not ask what your tool scored; they ask why this candidate was closed, and a stored sentence answers that three years later when the analyst has left.

    A re-screen is a new search, triggered by the coverage timestamp

    An intentional re-screen uses a fresh Idempotency-Key and creates a new search_id with its own billing and dated result. If the request times out, retry with the same key and payload to recover that search instead of starting another paid run. A changed payload under an existing key is a conflict. Key your scheduling queue on (subject_id, list_version) so a subject is re-screened once per data change rather than once per cron tick.

    GET /api/v1/coverage gives you the version signal:

    {
      "status": "ok",
      "checkedAt": "2026-09-04T09:20:03.418662+00:00",
      "domains": {
        "sanctions": { "domain": "sanctions", "expected_tables": 51, "available_tables": 51, "tables": ["…"], "latest_updated_at": "2026-09-04T04:11:52+00:00" },
        "criminal_watchlists": { "domain": "criminal_watchlists", "available_tables": 16, "tables": ["…"], "latest_updated_at": "2026-09-03T22:40:07+00:00" },
        "pep": { "domain": "pep", "available_tables": 2, "tables": ["…"], "latest_updated_at": "2026-09-01T03:15:44+00:00" }
      }
    }
    

    Counts and timestamps above are illustrative — call the endpoint for the live ones. The field that matters is latest_updated_at, and it is per domain because sanctions, criminal watchlists and PEP refresh on different cadences. When latest_updated_at for a domain moves past the timestamp of a stored snapshot, that snapshot is stale by definition. At €0.39 per check with search_type: sanctions_check, a 2,000-name book re-screened after a list movement costs €780. That is the number to put next to the cost of explaining a missed designation, not next to the cost of doing nothing.

    What this API will not do

    There is no official client library in any language, no webhooks and no callbacks. Polling is the only delivery mechanism and 202 is the only success response to a POST, so a working integration is one POST, one polling loop and one row of storage. There is no verdict field either. The response hands you a status per source, and the match/review/clear rollup is yours to compute and yours to own. There is no score, no threshold parameter and no "confidence" key. Errors arrive as a flat envelope, {"error": …, "message": …, "status_code": …}: 401 for a missing or invalid key, 403 for a key lacking search:write or search:read, and 429 with Retry-After when you outrun your tier.

    None of that is a gap to apologise for. A short integration is one your successor can still read. What you should insist on — from ScreenVeritAI or from anyone else — is what the three responses above name: the list, the reference, the measures, the date and the authority's own URL. The human who signs the disposition needs something to decide with.

    Screening supports compliance review and due diligence documentation. The disposition remains yours.

    Frequently asked questions

    Does the screening API return a match score?

    No. The v1 response represents each screened source as a finding with a Status line — Sanctioned, Potential PEP match, Potential watchlist match, or No relevant listing found — plus the matched entity, its identifiers, the programme or measures, the listing dates, the issuing authority and an evidence URL. There is no numeric score field on the response and no threshold parameter on the request, so there is no 0.82 to tune.

    How do I get the result of POST /api/v1/search?

    Poll. The POST returns HTTP 202 with a search_id and an estimated_time_seconds hint; you then call GET /api/v1/search/{search_id} until status is completed. There are no webhooks and no SDKs, so a client is a loop with a sleep and a timeout. Any status beginning with error_ is terminal — stop polling and log it.

    What threshold should I set for a sanctions screening API?

    You do not set one on the request. What you set is your review policy: which Status values enter a human queue, who reviews them, and how fast. The only numeric threshold in the pipeline works in the opposite direction — Full Search's review stage discards a candidate only when it is at least 0.95 confident the candidate is a false positive and can state why. Nothing is ever raised to you because a similarity number cleared a bar.

    Does sending a country or an entity type narrow the match?

    Not for list screening. The request accepts entity_type and country, and both are stored and echoed back on the search record, but the sanctions, PEP and criminal-watchlist matching runs on the name. The country field sharpens identity resolution for the Deep Research Report search type only. Treat both as metadata you will be glad to have in your own audit trail, not as filters.

    What does a no-hit response actually say?

    Each per-source field reads "No relevant listing found" for that source, with the line "No relevant listing was found in this source for the searched entity". It does not say the subject is clean, safe or cleared, because a screening can only speak about the sources it screened on the day it ran. Record it as evidence of a check, not as a certificate.

    How do I re-screen a name after a list changes?

    Create a new POST with a fresh Idempotency-Key for an intentional re-screen. Reuse the same key and payload only when recovering that request after a timeout; replay returns its original search instead of starting another paid run. Keep a dated result for each intentional check and key your scheduling queue on (subject_id, list_version). GET /api/v1/coverage exposes latest_updated_at per domain as a scheduling signal.

    What are the rate limits?

    They are per key and tier-based: 10 requests per minute and 100 per day on Free, 60 and 1,000 on Standard, 300 and 10,000 on Premium, 1,000 and 100,000 on Enterprise. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 adds Retry-After. Polling counts against those limits, so poll on a back-off rather than a tight loop.

    Sources

    1. ScreenVeritAI API v1 — public reference — ScreenVeritAI, September 4, 2026
    2. ScreenVeritAI API v1 — OpenAPI schema — ScreenVeritAI, September 4, 2026
    3. Specially Designated Nationals and Blocked Persons List (SDN) — human-readable lists — Office of Foreign Assets Control, September 4, 2026
    4. OFAC Sanctions List Service — Office of Foreign Assets Control, September 4, 2026
    5. Overview of sanctions and related tools (EU consolidated financial sanctions) — European Commission, DG FISMA, September 4, 2026
    6. United Nations Security Council Consolidated List — United Nations Security Council, September 4, 2026

    Run screening|All analyses

    Informational analysis of published regulatory sources. Not legal advice. Verify the primary sources before acting.