Documentation · Screening API

Integration checklist

What to decide, build and test before Crown Intelligence screening goes live in your system: the collection, the field mapping, the match threshold, how possible matches are reviewed and logged, how records stay current, and how the integration handles limits and errors.

The screening API needs no key today and answers anonymous requests. The checks below use the endpoints in the Screening API guide and the API reference. Automated or systematic screening, and use in a business process, need a commercial licence under our terms of use. Confirm that before you connect a live system.

Choose a collection

Each collection is a separate match path. Pick the one that matches the lists your obligations cover. A register is not a collection on its own; to narrow a search to one register, use include_dataset.

CollectionMatch pathContents
default/api/match/defaultEvery active record: sanctions, PEPs, debarment, enforcement and other risk lists.
sanctions/api/match/sanctionsSanctions listings only.
peps/api/match/pepsPolitically exposed persons.
crime/api/match/crimeDebarment, tax warrants and insolvency notices.
maritime/api/match/maritimeVessels, and records that carry an IMO number.

Map customer fields to the match request

A match request sends up to 20 named queries in one body, each with a schema and properties. Use your own customer or counterparty ID as the query key, so every response maps back to your record.

Your fieldMatch fieldRules
Name, or each spelling you holdproperties.nameArray of up to 5 strings. Add a transliterated form when you hold one. Required unless imoNumber or mmsi is sent.
Date of birthproperties.birthDateYYYY, YYYY-MM or YYYY-MM-DD. Send the precision you hold; a year alone is valid.
Nationality (person)properties.nationalityTwo-letter ISO 3166 code, such as ca or ru.
Country of incorporation (company)properties.countryTwo-letter code. The same field also takes a vessel's flag.
IMO number (vessel)properties.imoNumberSeven digits, such as 9418286.
MMSI (vessel)properties.mmsiNine digits.
Type of partyschemaPerson, Company, Organization, LegalEntity or Vessel. A record of a different type lowers the score (type_mismatch). Send the type you know rather than LegalEntity by default.

A starting point that maps a customer record, sends a batch with retries, and prints the top result for each party.

import os, time, json, requests

BASE = os.environ.get("CROWN_API_BASE", "https://api.crownintelligence.net")
session = requests.Session()
session.headers["User-Agent"] = "my-screening-tool/1.0 ([email protected])"


def to_query(customer):
    """Map one customer record to a match query (see the field table above)."""
    props = {"name": customer["names"][:5]}
    if customer.get("birth_date"):
        props["birthDate"] = [customer["birth_date"]]        # YYYY, YYYY-MM or YYYY-MM-DD
    if customer.get("nationality"):
        props["nationality"] = [customer["nationality"]]     # ISO 3166-1 alpha-2, e.g. "ca"
    if customer.get("imo"):
        props["imoNumber"] = [customer["imo"]]               # seven digits
    return {"schema": customer["schema"], "properties": props}


def match(collection, queries, limit=5, threshold=0.75, changed_since=None, retries=3):
    """POST /api/match with retries for 429 (honour Retry-After) and 5xx (back off)."""
    params = {"limit": limit, "threshold": threshold}
    if changed_since:
        params["changed_since"] = changed_since
    for attempt in range(retries + 1):
        r = session.post(f"{BASE}/api/match/{collection}", params=params,
                         json={"queries": queries}, timeout=60)
        if r.status_code == 429 and attempt < retries:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        if r.status_code >= 500 and attempt < retries:
            time.sleep(2 ** attempt)
            continue
        r.raise_for_status()
        return r.json()


customers = {
    "C-1001": {"schema": "Person", "names": ["Igor Putin", "Игорь Путин"], "birth_date": "1953-03-30", "nationality": "ru"},
    "C-1002": {"schema": "Vessel", "names": ["Selam"], "imo": "9418286"},
    "C-1003": {"schema": "Person", "names": ["Zz Crown Checklist Testperson"], "birth_date": "1980", "nationality": "ca"},
}
queries = {cid: to_query(c) for cid, c in customers.items()}
out = match("default", queries, limit=3, threshold=0.75)

for cid, answer in out["responses"].items():
    print(cid, answer["status"], "candidates:", answer["total"])
    for hit in answer["results"][:1]:
        print("   top:", hit["caption"], hit["score"], "match" if hit["match"] else "no match", hit["datasets"][:1])
print("checked_at:", out["checked_at"], "| collection:", out["collection"], "| threshold:", out["threshold"])

Set the threshold and read the match flag

Each result has a score from 0 to 1 and a match flag, which is true when the score is at or above threshold. The default threshold is 0.75 and the default limit is 5 results per query (up to 50). Results below the threshold still come back with match set to false, so reviewers can see near misses.

FeatureWhat it measuresEffect on the score
name_matchWord-by-word alignment of the query name with the record's names and aliases.Sets the base score.
dob_matchBirth date, at the record's precision.+0.10 on a match; 0 when only the year matches; −0.25 when different.
country_matchNationality or country code.+0.05 on a match; −0.10 when different.
identifier_matchIMO number.At least 0.98 on a match; −0.30 when different.
type_mismatchThe record is a different type from schema.−0.20.

Handle possible matches

A result with match set to true is a possible match, not a finding. Route it to a review queue, and keep a decision for each one.

Log what was checked

We do not store query bodies, so your system must keep its own record of every check, including checks with no match. Store the response as received, and add the fields below.

Field to logWhere it comes from
Your customer or counterparty IDYour system.
Query as sentYour request body.
Collection and its versionThe collection name you used, and its version from /api/catalog at the time of the check.
Algorithm, threshold and limitThe query parameters you sent. The response echoes algorithm, threshold and limit.
Time of the checkchecked_at in the response (UTC).
Record ID, merged entity, score and featuresEach result's id, merged_entity, score and features.
Registers, topics and programmeEach result's datasets, topics and programme.
Decision, reviewer and decision dateYour review queue.

Re-screen and monitor

Lists change. A party cleared last month can match after a new listing. Re-screen on a schedule, and when a collection is rebuilt.

import os, json, requests

BASE = os.environ.get("CROWN_API_BASE", "https://api.crownintelligence.net")

# Versions now, to compare with the versions stored at the last run
catalog = requests.get(f"{BASE}/api/catalog", timeout=30).json()
versions = {c["name"]: c["version"] for c in catalog["collections"]}
last_run = {"sanctions": "20261004022212-jhp"}  # the version stored at the last run
changed = [name for name, v in versions.items() if last_run.get(name) not in (None, v)]
print("collections rebuilt since the last run:", changed)

# Only records added or changed since the last run (ISO 8601 date or time)
body = {"queries": {"C-1001": {"schema": "Person", "properties": {"name": ["Igor Putin"], "birthDate": ["1953"]}}}}
r = requests.post(f"{BASE}/api/match/sanctions", params={"limit": 10, "changed_since": "2026-10-01"},
                  json=body, timeout=60)
r.raise_for_status()
answer = r.json()["responses"]["C-1001"]
print("changed since 2026-10-01:", answer["total"], "candidates;", r.json()["filters"]["changed_since"])

# Entity lookup for the review screen
ent = requests.get(f"{BASE}/api/entity/379781867", timeout=30).json()
print(ent["id"], ent["name"], ent["register"], ent["list_type"], ent["listings"][0]["register"])

Rate limits, retries and volume

The data service has a per-address request limit that the operator can switch on. It is off today, and no figure is published. The terms reserve the right to apply limits or volume caps to automated use, so agree your volumes with us before you send a large batch.

To be provided: The request limit that will apply to anonymous and licensed use, if any. Key issuance, the request header and the key format, once keys are introduced. Do not build against an assumed format.

Test with known names

Test the integration against names with known listings before go-live, and keep the test set in version control. Include:

One check we ran for this page: the query Igor Putin, birth year 1953 and nationality ru, against the sanctions collection with a threshold of 0.8 returned the sanctions listing at score 1 with match set to true. Results depend on the build in use, so record the collection version with each test run and repeat the tests after each rebuild.

Go-live checks

Samples

The samples below are starting points, so run them in a test environment before you rely on them. Set CROWN_API_BASE to change the base address; the default is the public service.

Node.js: match request

const BASE = process.env.CROWN_API_BASE ?? "https://api.crownintelligence.net";
const queries = {
  vessel: { schema: "Vessel", properties: { name: ["Selam"], imoNumber: ["9418286"] } },
  person: { schema: "Person", properties: { name: ["Igor Putin"], birthDate: ["1953-03-30"], nationality: ["ru"] } },
};
const params = new URLSearchParams({ limit: "3", threshold: "0.75" });
const res = await fetch(`${BASE}/api/match/maritime?${params}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ queries }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
for (const [key, answer] of Object.entries(data.responses)) {
  const top = answer.results[0];
  console.log(key, answer.status, answer.total, top ? [top.caption, top.score, top.match] : "no hits");
}

curl: collection versions and one match

# Versions of each collection, to see when a list was rebuilt
curl -s "$CROWN_API_BASE/api/catalog" | python3 -c 'import json,sys; [print(c["name"], c["version"]) for c in json.load(sys.stdin)["collections"]]'

# One party against the sanctions collection
curl -s -X POST "$CROWN_API_BASE/api/match/sanctions?limit=3&threshold=0.8&topics=sanction" \
  -H "Content-Type: application/json" \
  -d '{"queries":{"q1":{"schema":"Person","properties":{"name":["Igor Putin"],"birthDate":["1953"],"nationality":["ru"]}}}}'
To be provided: Guidance on which collection to use for each obligation. Crown Intelligence does not decide your screening policy.

Last updated 2026-10-10