MilScreen

MilScreen API v1

Quickstart

Screen a name against U.S. and UN sanctions lists with one HTTPS request. The response is a receipt: the matches, why each one scored as it did, and the exact list versions used.

1. Get a test key

Test keys are free and run on the real lists. Email [email protected] for one. A key looks like ms_test_ followed by 32 letters and digits.

2. Screen a name

curl https://api.milscreen.com/v1/screenings \
  -H "Authorization: Bearer ms_test_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Ivan Petrov", "birth_date": "1975-03-02", "country": "RU"}'

3. Read the result

{
  "id": "scr_01JBX7Q9T4M2ZK8VQ3W5E6R7T8",
  "object": "screening",
  "created_at": "2026-10-22T14:03:11Z",
  "livemode": false,
  "status": "match",
  "matches": [
    {
      "list": "ofac_sdn",
      "entry_id": "12345",
      "name": "PETROV, Ivan Ivanovich",
      "score": 93,
      ...
    }
  ],
  ...
}

status is match when at least one entry scored at or above the threshold (85 unless you set it), and no_match otherwise. Keep the id: it is your record of what was screened, when, and against which list versions.

Conventions

  • Base URL https://api.milscreen.com. JSON in and out; POSTs need Content-Type: application/json.
  • Field names are snake_case. Timestamps are UTC in RFC 3339 with seconds: 2026-10-22T14:03:11Z. IDs are strings with a prefix: scr_ for screenings, req_ for requests.
  • Every documented field is always present. Missing values are null, and empty lists are [].
  • Ignore fields you do not know. New fields, new enum values, and new lists can appear at any time and are not breaking changes.
  • Every response has a Request-Id header. Quote it if you contact support.
  • Request bodies are limited to 256 KB.
  • The OpenAPI document is at api.milscreen.com/openapi.json.

Authentication and keys #

Send your key in the Authorization header on every /v1 request. It is the only place a key is accepted.

Authorization: Bearer ms_live_...
PrefixUselivemodeCostLimit
ms_live_Productiontrue1 credit per name1,200 names per minute
ms_test_Development and CIfalseFree100 names per hour
ms_pub_The milscreen.com search pagetrueFree100 names per hour per IP address
  • A request without the header gets missing_api_key. A malformed, unknown, or revoked key gets invalid_api_key. Both are HTTP 401.
  • A key is shown once, when it is created. MilScreen stores only a hash of it.
  • Keep live keys on your server. Anyone who can read a key can spend its credits.

Screening #

POST /v1/screenings

Screens one name and stores the result. Only name is required. The other fields lower the score of entries that disagree with them; they never create a match on their own.

FieldTypeRules
namestring, requiredA person, organization, vessel, or aircraft. 1 to 512 characters after trimming, with at least one letter or digit and no control characters.
birth_datestringYYYY, YYYY-MM, or YYYY-MM-DD; a real date from 1900 to this year. Compared against individuals only.
countrystringAn ISO 3166-1 alpha-2 code in any case (RU, ru) or an English country name (Russia). The receipt echoes the uppercase code.
entity_typestringindividual, organization, vessel, or aircraft. Entries of a different stated type are skipped; entries with no stated type are kept.
thresholdinteger50 to 100, default 85. An entry is a match when its score is at or above it. Must be a JSON integer: "85" and 85.0 are rejected.
listsarray of strings1 to 10 list IDs from GET /v1/lists. Default: every list with default: true.

An empty string or whitespace for birth_date, country, or entity_type means "not provided", as does null for any optional field. Spreadsheet blanks can be sent as they are. Any other key is rejected with unknown_parameter, so a typo such as birthdate cannot quietly drop a filter.

The receipt

The response is the stored screening, and GET /v1/screenings/{id} returns the same bytes later.

{
  "id": "scr_01JBX7Q9T4M2ZK8VQ3W5E6R7T8",
  "object": "screening",
  "created_at": "2026-10-22T14:03:11Z",
  "livemode": true,
  "query": {"name": "Ivan Petrov", "birth_date": "1975-03-02", "country": "RU", "entity_type": null},
  "threshold": 85,
  "status": "match",
  "matches": [
    {
      "list": "ofac_sdn",
      "entry_id": "12345",
      "entity_type": "individual",
      "name": "PETROV, Ivan Ivanovich",
      "matched_name": "PETROV, Ivan Ivanovich",
      "score": 93,
      "reasons": [
        {"field": "name", "points": 93, "detail": "primary name 'PETROV, Ivan Ivanovich': ivan=ivan, petrov=petrov; extra on entry: ivanovich"},
        {"field": "birth_date", "points": 0, "detail": "matches 1975-03-02"},
        {"field": "country", "points": 0, "detail": "matches RU"}
      ],
      "birth_dates": ["1975-03-02"],
      "countries": ["RU"],
      "programs": ["RUSSIA-EO14024"],
      "url": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=12345"
    }
  ],
  "lists": [
    {"id": "ofac_sdn", "version": "a3f9c2e1b7d4", "updated_at": "2026-10-21T16:52:10Z", "checked_at": "2026-10-22T14:00:04Z", "status": "current"},
    ...
  ],
  "algorithm": "2026-10-23",
  "disclaimer": "Informational only. Not a compliance determination. Verify any match against the official list."
}
FieldMeaning
queryThe inputs as MilScreen used them: trimmed, country as an alpha-2 code, null where not provided.
statusmatch if and only if matches is not empty.
matchesSorted by score (highest first), then list order, then entry_id. At most 50.
matches[].nameThe entry's primary name as published. matched_name is the name or alias that scored best.
matches[].score0 to 100, the sum of the reasons[].points.
matches[].reasonsOne line per field you sent: name first, then birth_date, country, and entity_type. Show detail to people, but do not parse it.
matches[].urlThe publisher's page for the entry or list. Use it to check the match at the source.
listsEvery list screened, with the version in use at that moment.
algorithmThe scoring version. It changes whenever scoring changes, so old receipts stay explainable.

How scores work

  • Name gives 0 to 100 points. Words are compared one by one: exact, close spellings, and initials. Rare words count for more than common ones such as "International" or "Trading", and extra words on the entry lower the score a little.
  • Birth date subtracts 20 when the entry is an individual with birth dates and none of them overlap yours. A year matches any date in that year.
  • Country subtracts 15 when the entry lists countries and yours is not among them.
  • Entity type adds no points. It skips entries of a different stated type.

Birth date and country can only lower a score. Send them when you have them: they are the main way to cut false positives on common names.

Retrieve a receipt

GET /v1/screenings/{id}

Returns the stored receipt, byte for byte. A key can read receipts of its own account and mode only: test keys cannot read live receipts, and live keys cannot read test receipts. Anything else is not_found.

curl https://api.milscreen.com/v1/screenings/scr_01JBX7Q9T4M2ZK8VQ3W5E6R7T8 \
  -H "Authorization: Bearer ms_test_..."

Batch #

POST /v1/screenings/batch

Screens up to 100 names in one request. Each item has the same fields as a single screening.

curl https://api.milscreen.com/v1/screenings/batch \
  -H "Authorization: Bearer ms_test_..." \
  -H "Content-Type: application/json" \
  -d '{"screenings": [
        {"name": "Ivan Petrov", "birth_date": "1975-03-02", "country": "RU"},
        {"name": "Acme Widgets LLC", "country": "US", "entity_type": "organization"},
        {"name": "Maria Lopez", "birth_date": "1980-13-01"}
      ]}'

The response has one element per item, in the same order. Each element is a screening or an error:

{
  "data": [
    {"id": "scr_01JBX7XG4R6T8V0W2Y4A6C8E0G", "object": "screening", "status": "match", ...},
    {"id": "scr_01JBX7XG4R6T8V0W2Y4A6C8E0H", "object": "screening", "status": "no_match", ...},
    {"error": {"type": "invalid_request_error", "code": "parameter_invalid", "param": "birth_date",
               "message": "birth_date must be YYYY, YYYY-MM, or YYYY-MM-DD.",
               "doc_url": "https://milscreen.com/docs#parameter_invalid", "request_id": "req_01JBX7XG3M5P7R9T1V3X5Z7B9D"}}
  ]
}
  • One bad row does not fail the others. Item errors use the normal error object, and param names the field within the item.
  • Only valid items count toward rate limits and credits.
  • A body without screenings, with an empty array, or with more than 100 items fails as a whole with 400.
  • Problems with the request itself (401, 402, 409, 413, 415, 429, and 5xx) still use their HTTP status. If the account has fewer credits than valid items, the whole batch fails with 402. Batches are never partly billed.

Lists and freshness #

GET /v1/lists

Every list MilScreen covers, with its current version and status. Reading lists is free and does not count toward rate limits.

{
  "data": [
    {
      "id": "ofac_sdn",
      "object": "list",
      "name": "Specially Designated Nationals and Blocked Persons List",
      "publisher": "U.S. Treasury, OFAC",
      "url": "https://sanctionslist.ofac.treas.gov/Home/SdnList",
      "default": true,
      "version": "a3f9c2e1b7d4",
      "updated_at": "2026-10-21T16:52:10Z",
      "checked_at": "2026-10-22T14:00:04Z",
      "entry_count": 19471,
      "status": "current"
    },
    ...
  ]
}
IDListPublisher
ofac_sdnSpecially Designated Nationals and Blocked Persons ListU.S. Treasury, OFAC
ofac_consConsolidated Sanctions List (non-SDN)U.S. Treasury, OFAC
bis_elEntity ListU.S. Commerce, BIS
bis_dplDenied Persons ListU.S. Commerce, BIS
bis_uvlUnverified ListU.S. Commerce, BIS
bis_meuMilitary End User ListU.S. Commerce, BIS
state_dtcITAR Debarred PartiesU.S. State, DDTC
state_acnNonproliferation SanctionsU.S. State, ISN
bis_dpl_expiredDenied Persons List, expired orders (opt-in)U.S. Commerce, BIS
un_scUN Security Council Consolidated ListUnited Nations Security Council
  • Every list is screened by default except bis_dpl_expired. An expired denial order restricts no one, so those entries are screened only when you name the list in lists, for example for deeper due diligence.
  • MilScreen checks each source every 15 minutes. A new version appears only when the content of a list changes: it is the first 12 hex characters of a hash of the parsed entries.
  • updated_at is when MilScreen first loaded that version. checked_at is the last time the source was confirmed to still serve it.
  • status is current when the list was checked within the last 2 hours, stale when it is loaded but has not been checked for 2 hours, and unavailable when no version is loaded. Unavailable lists have null version and times, and an entry_count of 0.
  • If a list you ask for is not loaded, the screening fails with list_unavailable rather than quietly skipping it.

Errors #

Every error, at every status, has the same shape. Branch on code: it never changes meaning, and the same code always comes with the same HTTP status. message is written for people and may change.

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "birth_date must be YYYY, YYYY-MM, or YYYY-MM-DD.",
    "param": "birth_date",
    "doc_url": "https://milscreen.com/docs#parameter_invalid",
    "request_id": "req_01JBX7RA2C5D6E7F8G9H0J1K2M"
  }
}
  • type is one of invalid_request_error, authentication_error, rate_limit_error, or api_error.
  • param is the request field to blame, or null. For the idempotency header it is Idempotency-Key.
  • request_id equals the Request-Id header.

invalid_json 400 · invalid_request_error

The body is not valid JSON, or it is JSON but not an object. Send one JSON object.

parameter_missing 400 · invalid_request_error

A required field is absent: name, or screenings for a batch. param names it.

parameter_invalid 400 · invalid_request_error

A field has the wrong type, format, or range. param names the field and message states the rule, as in the field table. Common causes: a date such as 03/02/1975 (send 1975-03-02), a country MilScreen does not recognize, a threshold sent as a string, or a list ID not in GET /v1/lists. A batch item that is not an object gets this code with param: null.

unknown_parameter 400 · invalid_request_error

The body has a key that is not in the schema. Check the spelling: the field is birth_date, not dob or birthdate.

missing_api_key 401 · authentication_error

No Authorization header. Send Authorization: Bearer <key>. See Authentication.

invalid_api_key 401 · authentication_error

The key is malformed, unknown, or revoked. Check for a missing character or a stray space, and that you are using the full key, not its ID.

insufficient_credits 402 · invalid_request_error

The account has fewer credits than the names in the request. Nothing was screened or billed. Add credits, or send fewer names.

not_found 404 · invalid_request_error

The path does not exist, or the screening ID does not belong to this key's account and mode.

method_not_allowed 405 · invalid_request_error

The path exists but not with this method. Screenings are created with POST and read with GET.

idempotency_key_reused 409 · invalid_request_error

This Idempotency-Key was already used in the last 24 hours with a different request. Use a new key for each distinct request. See Idempotency.

request_too_large 413 · invalid_request_error

The body is larger than 256 KB. Split the batch.

unsupported_media_type 415 · invalid_request_error

A POST without Content-Type: application/json.

rate_limit_exceeded 429 · rate_limit_error

The key's rate limit window is used up. Wait the number of seconds in the Retry-After header, then retry. See Rate limits.

internal_error 500 · api_error

Something failed on MilScreen's side. Retry with the same Idempotency-Key, so a retry is never billed twice. If it persists, email [email protected] with the request ID.

list_unavailable 503 · api_error

A list you asked for has no loaded version yet, usually just after a restart. Retry shortly, or pass lists without it. GET /v1/lists shows which lists are loaded.

connection_error client only · api_error

Raised by the MilScreen SDKs, never sent by the API: the request did not get a response, because of a network failure or a timeout. The SDKs retry it before giving up.

Rate limits and credits #

Limits count names screened, in fixed windows: one single screening is 1, and a batch is its number of valid items. Reading receipts and lists counts 0.

KeyLimitCounted per
ms_live_1,200 names per 60 secondskey
ms_test_100 names per hourkey
ms_pub_100 names per hourIP address

Every authenticated response carries the current state:

RateLimit-Limit: 1200
RateLimit-Remaining: 1187
RateLimit-Reset: 42
MilScreen-Credits-Remaining: 48210
  • RateLimit-Reset is the number of seconds until the window resets. A 429 adds Retry-After with the same value.
  • Live keys use 1 credit per name screened. Credits are prepaid at $1 per 1,000 names, with no contract or minimum. MilScreen-Credits-Remaining appears on live-key responses.
  • When a request needs more credits than the account has, it fails with insufficient_credits and nothing is billed.
  • Test keys and the publishable key are free.

Idempotency #

Screening is a pure computation, so the risk of a retry is paying twice. Send an Idempotency-Key header on POSTs, and a retry within 24 hours returns the original receipt without being billed again.

curl https://api.milscreen.com/v1/screenings \
  -H "Authorization: Bearer ms_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-8812-vendor-3" \
  -d '{"name": "Acme Widgets LLC"}'
  • A key is 1 to 255 printable ASCII characters. Keys are scoped to your account and mode.
  • Same key and same body: the stored response comes back with 200 and Idempotent-Replayed: true, and costs nothing.
  • Same key and a different body: idempotency_key_reused.
  • Only successful responses are stored, so retrying after an error runs the request again.
  • Keys expire after 24 hours. The header is optional; without it, a retry is a new screening.
  • The SDKs send a random key on every POST and reuse it across their own retries.

Test mode #

Test keys (ms_test_) use the same URLs, the same engine, and the same lists as live keys. The differences:

  • Receipts have "livemode": false, so they cannot be mistaken for production records.
  • They are free and limited to 100 names per hour.
  • Test receipts are separate from live ones: neither kind of key can read the other's.
  • Test receipts are deleted after 30 days.

There are no special test names. Because test keys run on the real lists, a name that is on a list today will match in test mode too. Lists change, so write tests that check the shape of a response rather than a specific entry.

Clients #

Every client is a thin wrapper over the endpoints above, so results are identical everywhere. None of them does its own matching.

Python

pip install milscreen
from milscreen import MilScreen

client = MilScreen()  # reads MILSCREEN_API_KEY
screening = client.screenings.create("Ivan Petrov", birth_date="1975-03-02", country="RU")
print(screening.status, [m.name for m in screening.matches])

Command line

The Python package includes the milscreen command. A batch reads a CSV with a header row and writes the same rows with result columns appended.

export MILSCREEN_API_KEY=ms_test_...
milscreen screen "Ivan Petrov" --birth-date 1975-03-02 --country RU
milscreen batch vendors.csv          # writes vendors-milscreen.csv
milscreen lists

TypeScript and JavaScript

Zero dependencies; runs on Node 20+, browsers, Cloudflare Workers, and Deno.

npm install milscreen
import { MilScreen } from "milscreen";

const client = new MilScreen(process.env.MILSCREEN_API_KEY);
const screening = await client.screenings.create({ name: "Ivan Petrov", country: "RU" });
console.log(screening.status);

Google Sheets

=MILSCREEN(A2), or =MILSCREEN(A2:A100, B2:B100, C2:C100) for a whole column with birth dates and countries. Each row returns four cells: status, score, the top match, and the screening ID, so keep three empty columns to the right. Set your key once from the MilScreen → Set API key menu. Editors of the sheet can read the key through Apps Script, so share the sheet with care.

Excel

=MILSCREEN.SCREEN(A2), with the same arguments and the same four result columns as Sheets. The add-in is served from https://milscreen.com/excel/; enter your key in its task pane.

Zapier

A Screen action that takes a name, birth date, country, and type, and returns the status, top score, top match, and screening ID for the next steps of your Zap.

OpenAPI

The full contract is at api.milscreen.com/openapi.json. Use it to generate a client in another language.

Scope and limitations #

Informational only. Not a compliance determination. A match means a name scored above your threshold against a list entry. It does not mean the person or company is that entry. A no_match means no entry scored above the threshold on the lists screened. It is not a finding that a party may be dealt with. Verify any match against the official list, using the url on the match.

  • Lists. The U.S. and UN lists in Lists and freshness. UK, EU, and other national lists are not covered yet.
  • Not covered: politically exposed persons (PEPs), adverse media, and the OFAC 50 percent rule. A company owned 50 percent or more by blocked persons is blocked even when it is not named on a list, and MilScreen will not flag it unless it is listed itself.
  • Names. Scores compare names as published, including aliases. Transliterations, nicknames, and partial names can score lower than you expect, and common names can score high against unrelated entries. Sending a birth date and country helps with both.
  • Freshness. Each source is checked every 15 minutes, so a list change usually reaches results within about 15 minutes of the publisher's release. Each receipt records the version used, and status shows when a list has not been checked recently.

Data retention #

DataKept for
Receipts from live keysKept, so you can retrieve them for your records.
Receipts from test keys30 days, then deleted.
Receipts from the milscreen.com search page24 hours, then deleted.
Idempotency keys and the responses stored with them24 hours.
Request logsMethod, path, status, timing, key ID, and the number of names. Logs never contain names, birth dates, or other request content.

On milscreen.com, an uploaded spreadsheet is read in your browser and is never sent as a file. Only the name, birth date, country, and type of each row are sent for screening, and the results file is built in your browser. An API key entered on the site stays in that browser tab's session storage.

Changelog #

2026-10-23
API v1: single and batch screening, stored receipts, and list status. Lists: OFAC SDN and non-SDN, six Commerce and State lists, the expired Denied Persons orders (opt-in), and the UN Security Council list. Scoring algorithm 2026-10-23.

Additive changes (new fields, new enum values, new lists) are listed here too. They do not need a new API version.