DEVELOPER GUIDES · GUIDEUPDATED 2026-09-04
    Developer Guides

    Criminal Watchlist Screening API for Interpol and FBI

    Screen names against Interpol Red Notices and FBI Most Wanted over REST. POST /api/v1/search with X-API-Key, poll, read the criminal_watchlists field.

    Criminal and wanted-person watchlists are screened in every check rather than sold as their own endpoint, and the field that carries them does not behave like the sanctions fields: criminal_watchlists is null when nothing matched, not a formatted no-match block. Get that wrong and a null reads as a clean result in your code, which is the one bug worth catching before you ship. POST /api/v1/search with search_type sanctions_check (€0.39) screens the name against sanctions lists, PEP records and criminal watchlists in one pass, on a key holding the search:write permission. Everything runs asynchronously, so HTTP 202 hands back a search_id and you poll GET /api/v1/search/{search_id} until status is completed. There is no per-list filter, no SDK, no webhook and no score. What you get instead is a markdown section per matched source and a status line reading *Potential watchlist match* rather than *Sanctioned*, because a wanted-person listing is not a designation and collapsing the two would push a false positive into an asset-freeze workflow. Interpol coverage is the public Red Notice extract, queried live; the other sources are refreshed snapshots held as criminal_<source_id> tables you can enumerate through GET /api/v1/coverage, which is how you show later which watchlist sources were actually in place on the day you screened.

    What this workflow covers

    SCOPE
    • Request fields: entity_name (required, 1–500 characters), entity_type (person, company or any), search_type and the optional country, identifier, website and language fields. There is no separate watchlist endpoint, and no per-list filter in the request model.
    • The 202 response is search_id, status, message and estimated_time_seconds, which is 15 for sanctions_check and 30 for full_search. Both numbers are constants in the code, not measurements.
    • Watchlist findings arrive in the criminal_watchlists field of GET /api/v1/search/{search_id}, as markdown. The section heading names the source: 'Interpol Red Notices Findings', 'FBI Most Wanted Findings', 'SAPS Most Wanted Findings' and so on.
    • A watchlist hit reads *Potential watchlist match*, never *Sanctioned*, and adds matched entity, identification, case details, authority and evidence URL lines where the source record carries them.
    • When nothing matches, criminal_watchlists is null rather than a no-match block, unlike the sanctions fields, which always return one. Null means no listing found in the checked watchlist sources.
    • Interpol is a live lookup against the public Red Notice service at ws-public.interpol.int. Those findings carry the source_id interpol_red_notices, and the coverage is the published Red Notice extract, not Interpol's internal database.
    • GET /api/v1/coverage returns the criminal_watchlists domain with available_tables, the table names (criminal_us_fbi_most_wanted, criminal_za_saps_most_wanted and the rest) and latest_updated_at.
    • Watch for completed. Before it you see processing, or queued for a deep_research_report, and the failure modes are error_queue_create, error_credit_deduction, error_streaming, error_saving_results and error_no_final_payload.
    • Per-key rate limits by tier: free 10/minute and 100/day, standard 60 and 1,000, premium 300 and 10,000, enterprise 1,000 and 100,000. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 adds Retry-After.
    • The error envelope is 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.
    • There is no idempotency key, so a retried POST creates and bills a second search.
    • Price per check: €0.39 for sanctions_check and €5.90 for full_search, which adds transliteration and alias expansion. Evidence PDFs come out of screening history in the portal; /api/v1 has no PDF endpoint.

    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"
      }'
    
    # HTTP/1.1 202 Accepted
    # {
    #   "search_id": "a1c9f4e7-3b62-48d0-9c15-2f7e6a8b0d33",
    #   "status": "processing",
    #   "message": "Search queued successfully. Poll the GET endpoint for results.",
    #   "estimated_time_seconds": 15
    # }
    
    # Enumerate the watchlist sources that were actually in place:
    curl https://screenveritai.com/api/v1/coverage \
      -H "X-API-Key: svai_your_key_here"
    GET /api/v1/search/{search_id} — no watchlist match, then a Red Notice match. Fictitious examples, abridged.json
    // No watchlist listing found — criminal_watchlists is null, not a block.
    {
      "id": "a1c9f4e7-3b62-48d0-9c15-2f7e6a8b0d33",
      "entity_name": "Test Person",
      "entity_type": "person",
      "status": "completed",
      "search_type": "sanctions_check",
      "criminal_watchlists": null,
      "pep": null,
      "ofac_sanctions": "## OFAC Sanctions Findings\n\n**Status:** *No relevant listing found* ...",
      "created_at": "2026-09-04T10:02:18.640000+00:00",
      "completed_at": "2026-09-04T10:02:33.915000+00:00"
    }
    
    // Potential match on the public Red Notice extract.
    {
      "id": "d5e30b18-7c44-4a91-8b02-91ff5c6d7e20",
      "entity_name": "Second Test Person",
      "entity_type": "person",
      "status": "completed",
      "search_type": "sanctions_check",
      "criminal_watchlists": "## Interpol Red Notices Findings\n\n**Status:** *Potential watchlist match*\n\n**Details:**\n- **Matched entity:** SECOND TEST PERSON\n- **Identification:** 1979-04-11 | Example nationality\n- **Case details:** 2019/12345\n- **Authority:** Interpol\n- **Evidence URL:** https://www.interpol.int/How-we-work/Notices/Red-Notices/View-Red-Notices\n\n**Source:** *Interpol*",
      "pep": null,
      "created_at": "2026-09-04T10:11:02.311000+00:00",
      "completed_at": "2026-09-04T10:11:19.774000+00:00"
    }
    GET /api/v1/coverage — the criminal_watchlists domain, abridgedjson
    {
      "status": "ok",
      "checkedAt": "2026-09-04T10:14:51.220000+00:00",
      "domains": {
        "criminal_watchlists": {
          "domain": "criminal_watchlists",
          "expected_tables": null,
          "available_tables": 3,
          "tables": [
            "criminal_gb_nca_most_wanted",
            "criminal_us_fbi_most_wanted",
            "criminal_za_saps_most_wanted"
          ],
          "latest_updated_at": "2026-09-03T02:14:07.118000+00:00"
        }
      }
    }
    
    // available_tables and tables reflect what is on disk at request time, so this
    // is the assertion to file with an audit: which watchlist sources were in
    // place, and when each was last refreshed. Interpol Red Notices are a live
    // lookup rather than a stored table, so they do not appear in this list.
    Poll and read the watchlist section with requestspython
    import os
    import time
    import requests
    
    BASE = "https://screenveritai.com/api/v1"
    HEADERS = {"X-API-Key": os.environ["SCREENVERITAI_API_KEY"]}
    
    created = requests.post(
        f"{BASE}/search",
        headers=HEADERS,
        json={
            "entity_name": "Test Person",
            "entity_type": "person",
            "search_type": "sanctions_check",
        },
        timeout=30,
    )
    created.raise_for_status()
    search_id = created.json()["search_id"]
    
    while True:
        response = requests.get(f"{BASE}/search/{search_id}", headers=HEADERS, timeout=30)
        response.raise_for_status()
        result = response.json()
        if result["status"] == "completed":
            break
        if result["status"].startswith("error"):
            raise RuntimeError(f"search {search_id} ended in {result['status']}")
        time.sleep(3)
    
    # criminal_watchlists is null when nothing was found in the checked sources.
    watchlists = result["criminal_watchlists"]
    if not watchlists:
        print(search_id, "no listing found in the checked watchlist sources")
    else:
        # One "## <Source> Findings" section per matched source.
        sections = [block.strip() for block in watchlists.split("## ") if block.strip()]
        for section in sections:
            heading = section.splitlines()[0]
            print(search_id, "->", heading)      # e.g. "Interpol Red Notices Findings"
        print("route to manual review; a watchlist entry is not a designation")

    Key statistics

    DATA
    Criminal watchlists in a standard check
    Screened by default, not an add-on
    ScreenVeritAI coverage model
    Interpol coverage
    Public Red Notice extract, queried live
    INTERPOL public notices service
    Quick Check price per screened name
    €0.39 (search_type: sanctions_check)
    ScreenVeritAI pricing

    Compliance glossary

    TERMS
    search_id
    The UUID in the 202 response, and the only handle you get on a check. It addresses the result at GET /api/v1/search/{search_id} and identifies the stored snapshot that shows which watchlist sources were checked on that date.
    Match
    A watchlist record returned for the submitted name. It appears in criminal_watchlists as a section headed by the source, with the status line *Potential watchlist match* and the identification, case and authority lines the source record carries.
    Disposition
    A reviewer's recorded outcome for a candidate: confirmed, dismissed as a false positive, or escalated. Wanted-person records carry sparse identifiers, so watchlist candidates almost always need a person to decide rather than a rule.
    Point-in-time evidence
    The stored snapshot of a completed check, including the watchlist sources available that day. Because notices are withdrawn as often as they are added, the snapshot is the only reliable way to show what a check returned at the time it ran.

    Authoritative references

    SOURCES

    Frequently asked questions

    Q&A
    Q.01
    criminal_watchlists came back null. Does that mean the person is clean?
    It means no listing was found in the checked watchlist sources on that date, which is a narrower statement and the only one worth putting in your UI. Null is not an error either: the sanctions fields always return a block, criminal_watchlists and pep do not, so a truthiness check on the field is the correct hit test. If the word clean has crept into your reviewer screens, fix it before an auditor finds it.
    Q.02
    Is there any way to test this without paying per call?
    Partly. GET /api/v1/health is public, and GET /api/v1/coverage tells you which watchlist tables are loaded and when each was last refreshed, neither of which costs a screening. Past that, everything bills: there is no sandbox and every POST /api/v1/search charges the published price. Point your integration tests at coverage and capture one real €0.39 response as your fixture.
    Q.03
    How long should I wait before giving up on a check?
    Poll every 2–3 seconds and set a deadline of roughly four times the hint. The 202 gives estimated_time_seconds of 15 for sanctions_check and 30 for full_search, both fixed per-type constants rather than measured times. Interpol is a live lookup against a third-party service, so it is the part of the check most exposed to somebody else's availability. A run that never reaches completed goes to manual review, not into an approved pile.
    Q.04
    We only care about Red Notices. Can I skip FBI and the rest?
    Not in the request. One check always covers sanctions, PEP and criminal watchlists, so filter on your side: read the section heading inside criminal_watchlists, 'Interpol Red Notices Findings' or 'FBI Most Wanted Findings', and drop the sections you do not report on. Keep the full response in your record anyway. It costs nothing and it answers the awkward later question about what else was checked.
    Q.05
    The same name reaches us in three different spellings. What do I do?
    Use Full Search (search_type: full_search, €5.90). It expands the query, matches aliases and transliterations, reviews each candidate and flags likely false positives. This matters more on wanted lists than on sanctions lists, because those records are frequently transcribed from non-Latin scripts by whoever filed them, with no house style. sanctions_check will not do it for you.
    Q.06
    How often should we re-screen against wanted lists?
    On your policy cadence, and never on a client retry, because there is no idempotency key to stop a retry becoming a second billed search. Notices are withdrawn about as readily as they are added, so a result from six months ago says nothing about today. Keep every search_id: the stored snapshot is the only thing that shows a notice was absent on the date you looked.
    Q.07
    Do you push watchlist hits to us, or is it pull only?
    Pull only. No webhooks, no callbacks, no SDKs. The integration is REST with an X-API-Key header and client-side polling, documented at /api/v1/docs with a machine-readable schema at /api/v1/openapi.json you can generate a client from.
    Q.08
    What does the watchlist domain cost on top of sanctions?
    Nothing extra. It sits inside the per-check price: €0.39 for sanctions_check and €5.90 for full_search, both screening sanctions, PEP and criminal watchlists in the same call. There is no watchlist add-on and no platform fee, so the only pricing decision per name is which of the two products to run.