DEVELOPER GUIDES · GUIDEUPDATED 2026-09-04
    Developer Guides

    OFAC Screening API and SDN List API Reference

    Screen a name against OFAC's SDN list over REST: POST /api/v1/search with an X-API-Key header, poll the search id, then read the ofac_sanctions field.

    There is no OFAC-only endpoint and no per-list filter in the request model. One call to POST /api/v1/search runs a Quick Check (search_type: sanctions_check, €0.39) against sanctions lists, PEP records and criminal watchlists in one pass, and the OFAC part comes back in the ofac_sanctions field; if your policy only covers OFAC, read that field and drop the rest on your side. The call is asynchronous. You get HTTP 202 with a search_id and poll GET /api/v1/search/{search_id} until status is completed, because the check fans out to live sources and holding the connection open would only move the timeout into your code. Authenticate with an X-API-Key header; keys come from the dashboard, or from POST /api/v1/keys with your account bearer token. The API has no SDKs, no webhooks and no numeric risk score. The first two do not exist, so your client is whatever HTTP library you already use and your completion signal is the poll loop. The third is a decision: a finding arrives as markdown carrying the matched entity, the authority and an evidence URL, because a score would hide the line an auditor asks to see. Keep the search_id. A completed search is stored as a snapshot and is not recalculated when OFAC next amends the SDN list, which is what lets you reproduce the result months later. Full Search (search_type: full_search, €5.90) runs the same list checks plus transliteration, alias expansion and an explainable review of every candidate match.

    What this workflow covers

    SCOPE
    • Request body: entity_name (required, 1–500 characters), entity_type (person, company or any), search_type, plus the optional country, identifier, website and language fields. The key needs the search:write permission.
    • The 202 body is search_id, status, message and estimated_time_seconds. That last value is a fixed per-type constant in the code (15 for sanctions_check, 20 for adverse_media, 30 for full_search, 1800 for deep_research_report), not a measured or guaranteed latency.
    • No per-list filter exists in the request model. Read the OFAC part from the ofac_sanctions field, or from the sanctions_sources entry whose source_id is ofac_sanctions.
    • GET /api/v1/search/{search_id} returns id, entity_name, entity_type, status, search_type, final_report, news_summary, one field per jurisdiction (ofac_sanctions, eu_sanctions, fr_sanctions, pl_sanctions, uk_sanctions, un_sanctions, canada_sanctions, australia_sanctions, switzerland_sanctions, south_africa_sanctions, new_zealand_sanctions), sanctions_sources, additional_sanctions_sources, criminal_watchlists, pep, relationships, created_at and completed_at.
    • Each sanctions_sources entry carries source_id, title, authority, details and hit_count. hit_count on the ofac_sanctions entry is the machine-readable signal; details repeats the markdown that appears in the ofac_sanctions field.
    • A no-hit ofac_sanctions field is never empty. Its status line reads *No relevant listing found*; a hit reads *Sanctioned* and adds matched entity, authority and evidence URL lines. Parse the status line, not the presence of the field.
    • Terminal status is completed. Before it you see processing, or queued for a deep_research_report, and failures land in error_queue_create, error_credit_deduction, error_streaming, error_saving_results or error_no_final_payload.
    • Rate limits are per API key, with the ceiling set by the subscription tier: free 10/minute and 100/day, standard 60 and 1,000, premium 300 and 10,000, enterprise 1,000 and 100,000. X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset ride on every response; a 429 adds Retry-After.
    • One error envelope everywhere: error, message and status_code. 401 for a missing or invalid X-API-Key, 403 for a key without search:write or search:read, 404 for an unknown search_id, 400 for an unsupported search_type, 429 for rate limits, 500 for internal failures.
    • There is no idempotency key on the request. Two identical POSTs create two searches and bill twice.
    • GET /api/v1/search lists past searches with status, page and page_size filters, and DELETE /api/v1/search/{search_id} removes a record. The evidence PDF comes out of screening history in the portal against the same search_id; /api/v1 has no PDF endpoint.
    • Prices per check: €0.39 sanctions_check, €0.99 adverse_media, €5.90 full_search, €14.90 deep_research_report, with no platform fee. GET /api/v1/account/credits reports the remaining allowance and prepaid balance before you queue a run.

    Code samples

    API
    Create the check — curl with X-API-Keybash
    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": "Test Person",
        "entity_type": "person",
        "search_type": "sanctions_check",
        "country": "PL"
      }'
    
    # HTTP/1.1 202 Accepted
    # X-RateLimit-Limit: 60
    # X-RateLimit-Remaining: 59
    # X-RateLimit-Reset: 1788519240
    #
    # {
    #   "search_id": "8f2a1c34-0d6b-4f7e-9a52-6c1e0b3d5a77",
    #   "status": "processing",
    #   "message": "Search queued successfully. Poll the GET endpoint for results.",
    #   "estimated_time_seconds": 15
    # }
    GET /api/v1/search/{search_id} — no OFAC match. Fictitious example, abridged to the fields this page discusses.json
    {
      "id": "8f2a1c34-0d6b-4f7e-9a52-6c1e0b3d5a77",
      "entity_name": "Test Person",
      "entity_type": "person",
      "status": "completed",
      "search_type": "sanctions_check",
      "final_report": null,
      "news_summary": null,
      "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*",
      "sanctions_sources": [
        {
          "source_id": "ofac_sanctions",
          "title": "OFAC Sanctions",
          "authority": "",
          "details": "## OFAC Sanctions Findings ...",
          "hit_count": 0
        }
      ],
      "criminal_watchlists": null,
      "pep": null,
      "relationships": "",
      "created_at": "2026-09-04T09:12:44.118000+00:00",
      "completed_at": "2026-09-04T09:13:01.402000+00:00"
    }
    Same endpoint — SDN hit. Fictitious example; authority and evidence URL are copied from the source record.json
    {
      "id": "3b7d90e2-5a41-4c8f-b0d6-1e9c72af4415",
      "entity_name": "Example Trading Company",
      "entity_type": "company",
      "status": "completed",
      "search_type": "sanctions_check",
      "ofac_sanctions": "## OFAC Sanctions Findings\n\n**Status:** *Sanctioned*\n\n**Details:**\n- **Matched entity:** EXAMPLE TRADING COMPANY\n- **Authority:** US Treasury OFAC\n- **Evidence URL:** https://sanctionssearch.ofac.treas.gov/\n\n**Source:** *US Treasury OFAC*",
      "sanctions_sources": [
        {
          "source_id": "ofac_sanctions",
          "title": "OFAC Sanctions",
          "authority": "US Treasury OFAC",
          "details": "## OFAC Sanctions Findings ...",
          "hit_count": 1
        },
        {
          "source_id": "eu_sanctions",
          "title": "EU Sanctions",
          "authority": "",
          "details": "## EU Sanctions Findings ...",
          "hit_count": 0
        }
      ],
      "criminal_watchlists": null,
      "pep": null,
      "created_at": "2026-09-04T09:20:03.771000+00:00",
      "completed_at": "2026-09-04T09:20:19.088000+00:00"
    }
    Submit and poll with requestspython
    import os
    import time
    import requests
    
    BASE = "https://screenveritai.com/api/v1"
    HEADERS = {"X-API-Key": os.environ["SCREENVERITAI_API_KEY"]}
    
    
    def screen(entity_name, entity_type="person", country=""):
        created = requests.post(
            f"{BASE}/search",
            headers=HEADERS,
            json={
                "entity_name": entity_name,
                "entity_type": entity_type,
                "search_type": "sanctions_check",
                "country": country,
            },
            timeout=30,
        )
        created.raise_for_status()          # 401/403/429 raise here
        body = created.json()               # 202 Accepted
        search_id = body["search_id"]
        wait = body["estimated_time_seconds"]
    
        # estimated_time_seconds is a hint, so keep polling past it.
        deadline = time.time() + max(wait, 60) * 4
        while time.time() < deadline:
            result = requests.get(f"{BASE}/search/{search_id}", headers=HEADERS, timeout=30)
            result.raise_for_status()
            payload = result.json()
            status = payload["status"]
            if status == "completed":
                return payload
            if status.startswith("error"):
                raise RuntimeError(f"search {search_id} ended in {status}")
            time.sleep(3)
        raise TimeoutError(f"search {search_id} still {status}; route to manual review")
    
    
    result = screen("Test Person", country="PL")
    ofac = result["ofac_sanctions"] or ""
    sdn_hit = "**Status:** *Sanctioned*" in ofac
    # Machine-readable equivalent:
    sdn_hit = any(
        source["source_id"] == "ofac_sanctions" and source["hit_count"] > 0
        for source in result["sanctions_sources"]
    )
    print(result["id"], "OFAC hit" if sdn_hit else "no OFAC listing found")

    Key statistics

    DATA
    Quick Check price per screened name
    €0.39 (search_type: sanctions_check)
    ScreenVeritAI pricing
    Rate limit, standard tier
    60 requests/minute, 1,000/day per API key
    ScreenVeritAI API v1
    Sanctions jurisdiction fields on a search result
    11 named jurisdictions plus a sanctions_sources array
    ScreenVeritAI API v1 SearchResult model

    Compliance glossary

    TERMS
    search_id
    The UUID returned in the 202 response from POST /api/v1/search. It addresses the result at GET /api/v1/search/{search_id}, appears on the stored screening-history entry and on the evidence PDF, and is the reference to keep in your own customer or supplier record.
    Match
    A record from a screening source that the name search returned for the submitted entity. In the API response a match appears twice: as a formatted block inside the relevant field (matched entity, authority, evidence URL) and as a hit_count on the corresponding sanctions_sources entry.
    Disposition
    What a reviewer wrote down about a candidate match: confirmed, dismissed as a false positive, or escalated. The v1 API returns findings and has no field for writing a decision back, so dispositions live in the portal's screening history next to the stored snapshot.
    Point-in-time evidence
    A stored snapshot of a completed screening as it stood on the screening date, including the sources checked and the findings returned. It is never recalculated when a list changes later, which is what lets a reviewer reproduce the decision months afterwards.

    Authoritative references

    SOURCES

    Frequently asked questions

    Q&A
    Q.01
    Do you have a sandbox, or does every test call cost me €0.39?
    Every call bills, including the ones you fire while wiring up polling. There is no sandbox environment and no test key; keys are created against your live account, and GET /api/v1/health is public if all you need is a reachability probe. Budget around twenty sanctions_check calls for the integration. That is €7.80, and cheaper than building a mock that lies to you.
    Q.02
    How often should I poll, and when do I give up?
    Poll every 2–3 seconds; anything faster just burns rate limit against a job that has not moved. The 202 hands back estimated_time_seconds of 15 for sanctions_check and 30 for full_search, and those are fixed per-type constants in the code rather than measured times, so use them to size a deadline and not to promise anyone a number. When the deadline passes, route the search to manual review. A timeout is not a pass.
    Q.03
    Can I ask for OFAC only? We do not report on the other lists.
    You cannot, and you are not paying extra for them. The request model has no per-list filter, so a check always covers sanctions, PEP and criminal watchlists, and €0.39 is the price of all three. Read ofac_sanctions, or the sanctions_sources entry with source_id ofac_sanctions, and ignore the rest. Storing the whole response anyway costs you nothing and answers the later question about what else was checked.
    Q.04
    We get names in Cyrillic. Does sanctions_check transliterate them?
    No. sanctions_check is the deterministic mode and matches the string you submitted. Transliteration, alias matching and query expansion run in Full Search (search_type: full_search, €5.90), which also reviews every candidate and flags likely false positives. If your inbound names arrive in more than one script or spelling, spend the €5.90 rather than writing your own transliteration table; that is the part that fails quietly.
    Q.05
    I retried a POST that timed out and got billed twice. Is there an idempotency key?
    No, and nothing deduplicates behind it either: two identical POSTs create two searches and bill both. Treat the 202 as the commit point so a transport-level retry never crosses it, and hold your own reference next to the returned search_id. Re-screening should follow your policy cadence, not your HTTP client's retry policy.
    Q.06
    Is there a Python SDK, or do I write the client myself?
    You write it, and it is about forty lines. There are no official SDKs and no client libraries. The API is plain REST with JSON bodies and an X-API-Key header, documented interactively at /api/v1/docs with a machine-readable schema at /api/v1/openapi.json that a generator will turn into a client for your language.
    Q.07
    What does an OFAC check actually cost us per month?
    Multiply your check count by €0.39 for sanctions_check; that is the whole calculation, because there is no platform fee. Adverse media is €0.99, full_search €5.90 and a Deep Research Report €14.90. Monthly plans add a check allowance that the API spends before it touches the prepaid EUR balance, and GET /api/v1/account/credits reports both. Read it before you queue a few thousand calls, not after.