← Back to blog
APIDevelopersSanctions screening

Sanctions Screening API: A Developer's Guide

The Screen100 Team··9 min read
Developer writing code representing a sanctions screening API integration

Photo by Lukas Blazek on Pexels

If you're the engineer tasked with wiring sanctions checks into an onboarding flow, a payments pipeline, or an internal tool, you don't need another explainer about what OFAC is — you need to know how a sanctions screening API actually behaves under load, what its error modes look like, and how to call it without creating bugs that a compliance analyst discovers six months later. This guide covers the practical shape of that integration: authentication, request and response payloads, idempotent retries, rate limits, batch calls, and how to handle ongoing monitoring alerts once a subject is saved rather than screened once and forgotten.

TL;DR

  • Auth is a single API key sent as a bearer token — no OAuth dance, no key rotation ceremony beyond generating a new one.
  • A single screen is one POST with a name (plus optional type, minimum score and result limit); the response gives a result band, a numeric score, and cited hits.
  • Send an Idempotency-Key header on every POST so a network timeout or a retried request never double-screens or double-bills.
  • Batch screening handles up to 100 names per call — use it for bulk imports instead of looping single calls and burning your rate limit.
  • Rate limits and monthly quotas differ by plan tier; check your plan's published limits rather than assuming numbers, and read the response headers to know where you stand.
  • Ongoing monitoring pushes alerts by webhook (or email) when a saved subject's status changes on a list refresh, so you don't have to poll for it.

Authentication: one key, one header

Every request to Screen100's API is authenticated with a single API key sent as a bearer token in the Authorization header. There's no session, no OAuth handshake, no refresh token to manage — generate a key from your dashboard, keep it server-side, and send it on every call:

Authorization: Bearer sk_live_your_key_here

Test keys (prefixed sk_test_) behave identically to live keys, which matters more than it sounds — a common integration bug is building against a sandbox that silently behaves differently from production, then finding the difference the hard way during a live cutover. Because auth is a single static credential, treat it exactly like a database password: never in client-side JavaScript, never committed to a repo, and rotated immediately if it leaks. This is also the one place the OWASP API Security Top 10 is worth reading before you ship anything: broken object-level and function-level authorization has topped that list since it was first published, and the most common failure mode in practice isn't a clever attack, it's a key that ended up somewhere it shouldn't — a public repo, a logged request, a browser bundle.

The shape of a single screen

The core call is a POST with a name and, optionally, an entity type, a minimum match score, and a result limit. Here's a representative request and response:

curl https://screen100.com/api/v1/screen \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f9c2a11-4b3d-4e2a-9c31-88e1a7f0d9aa" \
  -d '{
    "name": "Ramzan Kadyrov",
    "type": "individual"
  }'

{
  "query": "Ramzan Kadyrov",
  "type": "individual",
  "result": "match",
  "hit_count": 1,
  "top_score": 0.97,
  "hits": [{
    "primary_name": "KADYROV, Ramzan Akhmatovich",
    "matched_name": "KADYROV, Ramzan Akhmatovich",
    "score": 0.97,
    "source": "OFAC_SDN",
    "programs": ["RUSSIA-EO14024"],
    "slug": "kadyrov-ramzan-akhmatovich-ofac-sdn-36832"
  }],
  "lists_searched": ["OFAC_SDN", "OFAC_CONS", "UN_SC"],
  "screened_at": "2026-07-18T00:00:00.000Z",
  "request_id": "req_..."
}

name is required (2–200 characters); type, min_score and limit are optional refinements. The important field for your calling code to branch on is result, which lands in one of three bands.

Result / score band What it means Recommended handling
clear (below 0.72)No meaningful similarity to any listed entityProceed automatically; log the result for the audit trail
possible_match (0.72–0.88)Meaningful name similarity, but not conclusive on its ownHold and route to a human reviewer, or re-query with more identifying detail (DOB, nationality)
match (0.88 and above)High-confidence hit against a listed entityBlock the action and escalate; do not let application code silently override this

Don't build your integration around match alone. It's tempting to write an if (result === "match") block() and treat everything else as safe to proceed, but that silently treats possible_match as equivalent to clear — which defeats the point of having three bands instead of a boolean. Route possible_match to a queue a human actually looks at.

Why does a sanctions screening API need idempotency keys?

Networks fail in the middle of requests, not just before or after them. A client sends a POST, the server processes it and screens the name, and then the response is lost to a timeout, a proxy restart, or a dropped connection before the client ever sees "match" or "clear" come back. The client, quite reasonably, retries. Without an idempotency mechanism, that retry is a second, independent screen — which for a metered API means you're billed twice for one logical check, and in a batch-import context can mean thousands of names get screened twice during a network blip.

This is exactly the failure mode Stripe designed idempotency keys to solve for payments, and it generalises directly to any POST that has a side effect worth protecting — screening a name and recording that screening for audit purposes counts. Their idempotent requests documentation lays out the pattern that's become close to an industry standard: the client generates a unique key (a v4 UUID is the usual choice) and sends it as an Idempotency-Key header; the server stores the result of the first request against that key and returns the identical result for any retry, rather than re-running the operation.

Screen100's API follows the same pattern. Send an Idempotency-Key header on a /v1/screen or /v1/batch POST, and a repeated request with that same key returns the original result without consuming a second unit of quota — the response includes an idempotent-replay header so your logging can tell the difference between a fresh screen and a replay. GET requests don't need one; they're idempotent by definition, since they don't change anything.

A retry bug that actually happened

A payments startup we spoke with while building this guide had wired sanctions screening into their merchant-onboarding service with a naive retry wrapper: any request that didn't return 200 within three seconds got retried up to twice, with no idempotency key attached. Under normal load this was invisible. During a spike in signups, though, their screening calls started queuing behind a slow downstream service, responses arrived just past the timeout window, and the retry wrapper fired — quietly double- and triple-screening a meaningful fraction of that day's onboarding batch. Nothing broke in an obviously visible way; the bug surfaced weeks later when someone doing a billing reconciliation noticed the screen count didn't match the signup count. The fix was one header, added to every POST, and a rule that retries always reuse the same idempotency key rather than generating a new one per attempt — reusing the key is what makes a retry safe rather than just less obviously duplicated.

Batch screening: don't loop single calls

If you're screening a list of names rather than one name from a live form, use the batch endpoint instead of calling /v1/screen in a loop. It takes up to 100 names in a single request and returns a summary alongside the per-name results, in input order:

curl https://screen100.com/api/v1/batch \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "names": ["Ramzan Kadyrov", "Acme Trading LLC"] }'

{
  "request_id": "req_...",
  "summary": { "total": 2, "matches": 1, "possible": 0, "clear": 1 },
  "results": [ /* one entry per name, same shape as a single screen */ ]
}

We've seen the opposite mistake too: a team doing a one-time bulk import of a legacy customer list — tens of thousands of names — wrote a script that called the single-screen endpoint in a tight loop, hit their plan's rate limit within the first few minutes, and spent the rest of the afternoon debugging what looked like an intermittent outage but was actually a self-inflicted 429. Batching a few thousand names into calls of a hundred at a time, with a short pause between calls, finished the same import in a fraction of the requests and never came close to the limit. If you're screening more than a handful of names in one operation, reach for batch first.

How do rate limits and quotas actually work here?

Like most metered APIs, Screen100 enforces two independent ceilings: a rolling requests-per-minute limit on the key (to protect the service from bursts) and a monthly quota on billable screens (tied to your plan). Both are visible on every response via headers — x-quota-limit and x-quota-remaining tell you the monthly picture, and a 429 response with a retry-after header tells you exactly how long to back off if you hit the per-minute ceiling. Exact numbers differ by plan and can change, so check your plan's published limits on the pricing page or the API reference rather than hard-coding a number you read once — the free tier's per-minute ceiling is intentionally conservative, and upgrading raises both the monthly quota and the per-minute allowance substantially.

Design your retry logic around the headers, not a guess. On a 429, respect retry-after rather than immediately retrying — a tight retry loop against a rate limit just extends the outage for yourself and, at scale, for other tenants on shared infrastructure. This is standard API-consumer etiquette described in most public API design guides, including Google's API design guidance on errors, which recommends exponential backoff with jitter for exactly this reason: synchronised retries from many clients can turn a brief rate-limit blip into a thundering-herd problem for the service they're all hitting.

Handling ongoing monitoring: webhook first, poll only as a fallback

A single screen tells you about the moment you ran it. If you're screening vendors, customers or counterparties you have an ongoing relationship with, ongoing monitoring re-screens saved subjects automatically as the underlying lists refresh, and the useful integration question becomes: how do you find out when something changes, days or months after the original screen?

The lower-friction pattern is a webhook: configure an endpoint, and Screen100 pushes an event to it the moment a monitored subject's re-screen produces a new match or a materially changed score, alongside the existing email alert. Your service receives the event and can act on it directly — open a case, ping a Slack channel, block a scheduled payment — without ever having to ask "has anything changed?" A webhook consumer should still be idempotent for the same reason a POST client should be: verify you haven't already processed that specific alert event before acting on it a second time, since webhook delivery for any provider is generally "at least once," not "exactly once."

If your infrastructure can't accept inbound webhooks (a common constraint in some internal or air-gapped environments), the fallback is polling your own record of saved subjects and their screening history on whatever cadence suits your review cycle. It's strictly less efficient — you're asking a question you don't yet know the answer to, on a timer, instead of being told the moment the answer changes — but it still beats the manual alternative of a human remembering to re-run a check.

A quick word on security posture for compliance data

Screening results and hit data are the kind of payload worth treating carefully even though they're not, by themselves, secret in the way a password is — a request log that captures full screening payloads including personal identifiers (names, dates of birth, nationalities) is sensitive data by most privacy frameworks' definitions, and the OWASP API Security Top 10 specifically calls out excessive data exposure and broken object-level authorization as the most common ways APIs leak more than they should. Practically, that means: don't log full request bodies at debug level in a shared logging system without redaction, don't expose your production API key in any client bundle, and if you're building a multi-tenant product on top of Screen100, make sure each tenant can only see their own screening history — never trust a client-supplied subject ID without checking it belongs to the caller.

Where the MCP server fits

Everything above describes calling the REST API directly from your own backend. If you're building an AI agent that needs to run these checks autonomously — during automated vendor approval, for instance — the same screening engine is exposed as a hosted MCP server, so an agent gets a typed, callable tool and a structured, citable result instead of reasoning about sanctions status in free text. Our guide to adding sanctions screening to an AI agent with MCP covers that integration path in detail, and the MCP docs have the setup steps. For the broader picture on why automated screening has become standard practice rather than a nice-to-have, see our guide to automated sanctions screening.

None of this is complicated once it's in place — a bearer token, a couple of endpoints, an idempotency header, and headers you read before you retry. The API reference has the full field-by-field detail, and a free key from the dashboard is enough to build and test the whole flow before you need a paid plan for batch volume or monitoring.

Frequently asked questions

How do I authenticate to the sanctions screening API?

Send a single API key as a bearer token in the Authorization header: Authorization: Bearer sk_live_your_key_here. There's no OAuth flow or session to manage — test keys behave identically to live keys, just against a sandbox.

Why do I need an idempotency key on a screening request?

If a request times out after the server has already processed it, a naive retry re-runs the screen — which can double-bill a metered API and double-write audit records. Sending an Idempotency-Key header, generated once per logical request and reused on every retry, makes the server return the original result instead of repeating the operation.

What's the difference between calling /v1/screen in a loop and using batch screening?

Looping single-screen calls for a list of names burns your per-minute rate limit fast and multiplies request overhead. The batch endpoint accepts up to 100 names in one call and returns a summary plus per-name results, which is both faster and far less likely to hit a rate limit during a bulk import.

How should my code handle a possible_match result, versus a match or clear?

Treat the three result bands differently: clear can proceed automatically, match should block the action and escalate to a human, and possible_match should be held for manual review rather than silently treated as safe. Building logic that only checks for match and lets everything else through defeats the purpose of having a middle band.

Does the API support webhooks for ongoing monitoring alerts?

Yes. Once a subject is saved for ongoing monitoring, a webhook (alongside an email alert) fires the moment a re-screen produces a new match or a changed score, so your system doesn't need to poll. Webhook consumers should still de-duplicate by event ID, since delivery is generally at-least-once rather than exactly-once.

Run this check on a real name

Free, no account required. Screen against the OFAC SDN, OFAC Consolidated and UN Security Council lists.