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 needContent-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-Idheader. 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_...
| Prefix | Use | livemode | Cost | Limit |
|---|---|---|---|---|
ms_live_ | Production | true | 1 credit per name | 1,200 names per minute |
ms_test_ | Development and CI | false | Free | 100 names per hour |
ms_pub_ | The milscreen.com search page | true | Free | 100 names per hour per IP address |
- A request without the header gets
missing_api_key. A malformed, unknown, or revoked key getsinvalid_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.
| Field | Type | Rules |
|---|---|---|
name | string, required | A person, organization, vessel, or aircraft. 1 to 512 characters after trimming, with at least one letter or digit and no control characters. |
birth_date | string | YYYY, YYYY-MM, or YYYY-MM-DD; a real date from 1900 to this year. Compared against individuals only. |
country | string | An ISO 3166-1 alpha-2 code in any case (RU, ru) or an English country name (Russia). The receipt echoes the uppercase code. |
entity_type | string | individual, organization, vessel, or aircraft. Entries of a different stated type are skipped; entries with no stated type are kept. |
threshold | integer | 50 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. |
lists | array of strings | 1 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."
}
| Field | Meaning |
|---|---|
query | The inputs as MilScreen used them: trimmed, country as an alpha-2 code, null where not provided. |
status | match if and only if matches is not empty. |
matches | Sorted by score (highest first), then list order, then entry_id. At most 50. |
matches[].name | The entry's primary name as published. matched_name is the name or alias that scored best. |
matches[].score | 0 to 100, the sum of the reasons[].points. |
matches[].reasons | One line per field you sent: name first, then birth_date, country, and entity_type. Show detail to people, but do not parse it. |
matches[].url | The publisher's page for the entry or list. Use it to check the match at the source. |
lists | Every list screened, with the version in use at that moment. |
algorithm | The 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
paramnames 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 with400. - 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"
},
...
]
}
| ID | List | Publisher |
|---|---|---|
ofac_sdn | Specially Designated Nationals and Blocked Persons List | U.S. Treasury, OFAC |
ofac_cons | Consolidated Sanctions List (non-SDN) | U.S. Treasury, OFAC |
bis_el | Entity List | U.S. Commerce, BIS |
bis_dpl | Denied Persons List | U.S. Commerce, BIS |
bis_uvl | Unverified List | U.S. Commerce, BIS |
bis_meu | Military End User List | U.S. Commerce, BIS |
state_dtc | ITAR Debarred Parties | U.S. State, DDTC |
state_acn | Nonproliferation Sanctions | U.S. State, ISN |
bis_dpl_expired | Denied Persons List, expired orders (opt-in) | U.S. Commerce, BIS |
un_sc | UN Security Council Consolidated List | United 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 inlists, for example for deeper due diligence. - MilScreen checks each source every 15 minutes. A new
versionappears only when the content of a list changes: it is the first 12 hex characters of a hash of the parsed entries. updated_atis when MilScreen first loaded that version.checked_atis the last time the source was confirmed to still serve it.statusiscurrentwhen the list was checked within the last 2 hours,stalewhen it is loaded but has not been checked for 2 hours, andunavailablewhen no version is loaded. Unavailable lists havenullversion and times, and anentry_countof 0.- If a list you ask for is not loaded, the screening fails with
list_unavailablerather 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"
}
}
typeis one ofinvalid_request_error,authentication_error,rate_limit_error, orapi_error.paramis the request field to blame, ornull. For the idempotency header it isIdempotency-Key.request_idequals theRequest-Idheader.
invalid_json
The body is not valid JSON, or it is JSON but not an object. Send one JSON object.
parameter_missing
A required field is absent: name, or screenings for a batch. param names it.
parameter_invalid
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
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
No Authorization header. Send Authorization: Bearer <key>. See Authentication.
invalid_api_key
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
The account has fewer credits than the names in the request. Nothing was screened or billed. Add credits, or send fewer names.
not_found
The path does not exist, or the screening ID does not belong to this key's account and mode.
method_not_allowed
The path exists but not with this method. Screenings are created with POST and read with GET.
idempotency_key_reused
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
The body is larger than 256 KB. Split the batch.
unsupported_media_type
A POST without Content-Type: application/json.
rate_limit_exceeded
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
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.
connection_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.
| Key | Limit | Counted per |
|---|---|---|
ms_live_ | 1,200 names per 60 seconds | key |
ms_test_ | 100 names per hour | key |
ms_pub_ | 100 names per hour | IP address |
Every authenticated response carries the current state:
RateLimit-Limit: 1200
RateLimit-Remaining: 1187
RateLimit-Reset: 42
MilScreen-Credits-Remaining: 48210
RateLimit-Resetis the number of seconds until the window resets. A429addsRetry-Afterwith 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-Remainingappears on live-key responses. - When a request needs more credits than the account has, it fails with
insufficient_creditsand 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
200andIdempotent-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
statusshows when a list has not been checked recently.
Data retention #
| Data | Kept for |
|---|---|
| Receipts from live keys | Kept, so you can retrieve them for your records. |
| Receipts from test keys | 30 days, then deleted. |
| Receipts from the milscreen.com search page | 24 hours, then deleted. |
| Idempotency keys and the responses stored with them | 24 hours. |
| Request logs | Method, 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.