Partner API v1 · Draft, not yet live

Matching: POST /v1/match

Draft — not yet live. This describes the proposed v1 partner contract (version 1.0.0-rc.1). No endpoint on this page is deployed, and no API key can be issued yet.

Send up to 100 queries in one request. Each query comes back with its own status, the time it was searched, the registers it was checked against and any matches at or above the minimum band.

Making a request

The required scope is match:read. Each call is charged one query unit per query in the batch. Send an Idempotency-Key on every screening so a retry returns the stored response instead of a second charge.

curl

curl https://api.crownintelligence.net/v1/match \
  -H "X-Api-Key: $CI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: screen-2026-10-13-0001" \
  -d '{
    "queries": [
      {"query_ref": "case-7731:party-1", "entity_type": "Person",
       "name": "Aleksandr Petrov", "dob": "1971-05-02", "country": ["RU"]},
      {"query_ref": "case-7731:vessel-1", "entity_type": "Vessel",
       "name": "Sea Lark", "identifiers": {"imo": "9176187"}}
    ],
    "options": {"min_band": "POSSIBLE", "limit": 5, "collections": ["default", "sanctions"]}
  }'

Node

const res = await fetch("https://api.crownintelligence.net/v1/match", {
  method: "POST",
  headers: {
    "X-Api-Key": process.env.CI_API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": "screen-2026-10-13-0001"   // reuse on retry, never on a new screening
  },
  body: JSON.stringify({
    queries: [
      { query_ref: "case-7731:party-1", entity_type: "Person",
        name: "Aleksandr Petrov", dob: "1971-05-02", country: ["RU"] }
    ],
    options: { min_band: "POSSIBLE", limit: 5 }
  })
});
if (!res.ok) throw new Error("HTTP " + res.status + " " + (await res.text()));
const { match_request_id, queries } = await res.json();

Query fields

Fields of one match query
FieldRule
query_refRequired. 1 to 64 characters from A to Z, a to z, 0 to 9, dot, underscore, colon and hyphen. Unique within the request. It is your opaque reference. Do not put a name, a date of birth or an identity number in it.
entity_typeRequired. One of Person, Company, Organization, Vessel, Airplane, LegalEntity.
name2 to 300 characters. Required unless the query has only identifiers.
aliasesOptional. Up to 10 names.
dob or dob_yearOptional: YYYY, YYYY-MM or YYYY-MM-DD in dob, or a year alone in dob_year. Not both.
countryOptional. Up to 5 ISO 3166-1 alpha-2 codes.
identifiersOptional. imo (7 digits), mmsi (9 digits), lei (ISO 17442), or wallet_address with chain and address.

The schema is closed. Any other field is refused with 400 invalid_request. A batch of more than 100 queries returns 413 batch_too_large.

Options

  • min_band: STRONG, POSSIBLE or WEAK. The default is POSSIBLE. See the bands on the overview.
  • collections: one to five of default, sanctions, peps, crime and maritime.
  • limit: matches per query, from 1 to 25. The default is 5.
  • include_features: whether each match lists the features that moved its score. The default is true.
  • algorithm: standard or name-only, as listed by the algorithms page.

Statuses

Every query has exactly one status. Read the statuses on the overview first: only no_hit_searched means no match was found, and a query is never clear while it is not_searched or error.

Statuses, matches and fields each status carries
StatusMatchesFields that are always present
hitOne or moreregisters_searched
no_hit_searchedNoneregisters_searched, with at least one register
not_searchedNonenot_searched_reason
errorNoneerror, with a code and a bilingual message

Some identifiers are accepted now but not searched until their register is loaded: lei and wallet_address. A query that has only these identifiers and no name returns not_searched, and the response lists them in identifiers_not_searched. A name searched against registers that do not cover its entity_type returns not_searched with the reason no_register_for_entity_type.

Response

The response lists one result per query, in the order sent, and a match_request_id. Use that id to open a case and to find the request in your logs.

{
  "match_request_id": "mrq_01J9Z3K4M6",
  "queries": [
    {
      "query_ref": "case-7731:party-1",
      "status": "hit",
      "searched_at": "2026-10-13T14:02:11Z",
      "registers_searched": [
        {"list_id": "crown-list-0412", "register_name": "EU Consolidated Financial Sanctions",
         "publisher": "European Union", "list_date": "2026-10-09", "loaded_at": "2026-10-12T22:40:00Z",
         "licence": "PERMITTED"}
      ],
      "matches": [
        {"record_id": "crown-88213", "list_id": "crown-list-0412", "band": "STRONG", "score": 0.94,
         "schema": "Person", "caption": "Aleksandr Petrov", "matched_name": "Aleksandr Petrov",
         "features": [
           {"field": "dob", "effect": "+0.10", "rule": "birth date match",
            "explanation": {"en": "Birth date matches.", "fr": "La date de naissance correspond."}}
         ],
         "record_url": "https://crownintelligence.net/records/crown-88213"}
      ]
    },
    {
      "query_ref": "case-7731:wallet-1",
      "status": "not_searched",
      "searched_at": "2026-10-13T14:02:11Z",
      "not_searched_reason": "identifier_not_covered",
      "identifiers_not_searched": ["wallet_address"],
      "registers_searched": [],
      "matches": []
    }
  ]
}

Each match carries record_id, list_id, band, score (0 to 1), schema, caption, matched_name, features and record_url. Each feature explains one change to the score, with field, effect, rule and a bilingual explanation. The list of rules can grow without a version change, so do not hard-code it.

Each entry in registers_searched gives list_id, register_name, publisher, list_date (the publisher's own date, or null), loaded_at (when Crown Intelligence last loaded the register) and licence. Judge freshness by loaded_at.

Handling each status

The pattern below sends every status to a different path. Nothing reaches a "clear" outcome except no_hit_searched.

// Only no_hit_searched can ever be a negative. Everything else needs a person or a retry.
function outcome(q) {
  switch (q.status) {
    case "hit":
      return { action: "review", matches: q.matches };
    case "no_hit_searched":
      return { action: "no match", registers: q.registers_searched.map(r => r.register_name), searchedAt: q.searched_at };
    case "not_searched":
      return { action: "not checked", reason: q.not_searched_reason };   // never clear
    case "error":
      return { action: "retry", code: q.error.code };                    // never clear
    default:
      throw new Error("unknown status: " + q.status);
  }
}

Always confirm a match against the official publication before acting on it. A match is a prompt for review, not a finding.

What is stored

Request bodies are not logged. Crown Intelligence keeps, per query, a keyed digest of the normalised name, date of birth and country codes, under a key unique to your account. The name and date of birth are not stored. Keep the link between your subject and query_ref in your own system. Retention periods are an open item for the owner.

Examples use illustrative names, references and record IDs.