API reference

Screening

Check a person or business against the loaded sanctions and PEP lists, review the matches, and see which lists are loaded.

Screening compares a name against the lists loaded on your install and returns possible matches, each with a score from 0 to 100 and the reasons for it. It decides nothing. A person reviews every match, and a potential match puts an alert in the queue so nobody has to go looking for it.

Only a synthetic sample list ships today. Sentinel doesn't include real sanctions or PEP data. You need to license a list from a data provider and load it yourself. The sample is made-up data for trying the API, and must never be used in production.

Loading lists

Lists are loaded with the admin command, which reads SENTINEL_DATABASE_URL:

Terminal
sentinel-admin screening load -file list.json

Loading a list with a code that's already loaded replaces it in one transaction, so a screening running at that moment sees the old list or the new one, never half of each. Lists are shared by every organisation on the install. Running instances pick up a new list within 30 seconds.

A list file looks like this. data/screening/sample-list.json in the repository is a full example.

list.json
{
  "code": "sample",
  "name": "Sample list (synthetic, for testing only)",
  "version": "2026-10-16",
  "entries": [
    {
      "ref": "SMP-0003",
      "kind": "person",
      "name": "Mohammed Sani Al-Rashid",
      "aliases": [
        "Muhammad Sani Rashid",
        "Mahamadu Sani Alrashid"
      ],
      "countries": [
        "NG",
        "NE"
      ],
      "category": "sanctions",
      "program": "SAMPLE",
      "birth_year": 1971
    }
  ]
}

code, name and version are required, and so are each entry's ref (unique in the file), kind (person or business), name and category (sanctions, pep or adverse_media). Countries are two-letter uppercase codes. The command checks the whole file before loading any of it and reports the first problem.

To try screening on a test install, set SENTINEL_SCREENING_SAMPLE=true. The API then loads the sample list on startup, but only when no list is loaded at all, so it never replaces or sits beside a real one.

How matching works

  • Names are compared word by word, by spelling and by sound, in any order. ASANTE, Kwame matches Kwame Asante, and Mohamed matches Muhammad.
  • Every alias on an entry is tried. matched_name is the one that matched best.
  • Titles are ignored. Particles such as bin and al count for less. Gold Crest and Goldcrest are the same name.
  • A birth year that's two or more years off lowers the score a lot. A country that isn't among the entry's countries lowers it a little. A birth year or country that agrees raises it a little.
  • People are only matched against people, and businesses against businesses.
  • With no list loaded, the API answers 503, never "clear".

Screen a name

POST /v1/screenings

Screens one person or business and stores the result, so it can be fetched later exactly as it was answered.

Request body

name
string required
The full name. 1 to 200 characters.
kind
string required
person or business.
birth_year
integer
For people. 1850 to 2100.
country
string
Two-letter country code, for example nationality or country of residence.
reference
string
Your own ID for this customer, kept with the result. Up to 100 characters.
threshold
integer
The lowest score to return, 50 to 100. Defaults to 80. A lower number finds more and needs more review.

Example

This uses the synthetic sample list.

Terminal
curl -X POST "$SENTINEL_URL/v1/screenings" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Muhammad Sani Rashid",
    "kind": "person",
    "birth_year": 1971,
    "country": "NG",
    "reference": "cus_1043"
  }'
Response · 200
{
  "id": "4c3b2a19-0f8e-4d7c-9b6a-5e4d3c2b1a09",
  "reference": "cus_1043",
  "status": "potential_match",
  "threshold": 80,
  "subject": {
    "name": "Muhammad Sani Rashid",
    "kind": "person",
    "birth_year": 1971,
    "country": "NG"
  },
  "matches": [
    {
      "score": 100,
      "list": "sample",
      "entry_ref": "SMP-0003",
      "entry_name": "Mohammed Sani Al-Rashid",
      "matched_name": "Muhammad Sani Rashid",
      "kind": "person",
      "category": "sanctions",
      "program": "SAMPLE",
      "countries": [
        "NG",
        "NE"
      ],
      "birth_year": 1971,
      "reasons": [
        {
          "code": "name_exact",
          "detail": "\"muhammad\" is the same word"
        },
        {
          "code": "name_exact",
          "detail": "\"sani\" is the same word"
        },
        {
          "code": "name_exact",
          "detail": "\"rashid\" is the same word"
        },
        {
          "code": "birth_year_match",
          "detail": "both born in 1971"
        },
        {
          "code": "country_match",
          "detail": "NG is one of the entry's countries"
        }
      ]
    }
  ],
  "lists": [
    {
      "code": "sample",
      "name": "Sample list (synthetic, for testing only)",
      "version": "2026-10-16",
      "entries": 25,
      "loaded_at": "2026-10-16T09:00:02.114Z"
    }
  ],
  "near": [],
  "resolutions": [],
  "review": "open",
  "alert_id": "e2f1a0b9-8c7d-4e6f-9a5b-4c3d2e1f0a9b",
  "created_at": "2026-10-16T10:41:27.630Z"
}

status is clear when nothing scored at or above the threshold, and potential_match otherwise. At most 25 candidates come back in total, highest score first.

Near misses

near holds candidates that scored at least 50 but under the threshold. They have the same fields as matches. They're there so a reviewer can see what came close, and they never make a screening a potential match. With a threshold of 50, near is always empty.

The alert

A potential_match raises an alert in the queue in the same request, and alert_id points to it. A clear screening has no alert. The alert is high priority if any match is on a sanctions list, and medium otherwise. See Screening alerts for how it looks in the queue.

Review state

review sums up what people have decided about the matches so far:

reviewMeaning
noneNothing to review. The screening is clear.
openAt least one match has no decision yet
pending_approvalA match was cleared and is waiting for a second person
clearedEvery match was cleared as a false positive, and each clearing was approved
confirmedSomeone confirmed a true match

resolutions lists every decision recorded about a match, oldest first. See Reviewing matches.

Reason codes

CodeMeaning
name_exactA word is the same
name_closeA word is spelled almost the same
name_phoneticA word sounds the same
name_initialA word could be an initial for the other
list_has_extra, query_has_extraOne name has a word the other doesn't
birth_year_match, birth_year_close, birth_year_differsHow the birth years compare
country_match, country_differsWhether the country is among the entry's countries

Use detail for display. It says which words were compared.

List screenings

GET /v1/screenings

Returns stored screenings, newest first, each in the same shape as the response above.

Query parameters

status
string
Only return screenings with this status: clear or potential_match.
reference
string
Only return screenings with this reference. Use it to see every check on one customer.
limit
integer
How many to return, 1 to 200. Defaults to 50. A value outside that range is a 400.
Terminal
curl "$SENTINEL_URL/v1/screenings?reference=cus_1043&limit=10" \
  -H "Authorization: Bearer $SENTINEL_KEY"

The response is { "screenings": [...] }.

Fetch a screening

GET /v1/screenings/{id}

Returns a stored screening. The subject, matches, near misses and list versions are exactly as they were first returned, even if the lists have changed since. That's what lets you explain a result later. resolutions, review and alert_id show where the review stands now.

Terminal
curl "$SENTINEL_URL/v1/screenings/4c3b2a19-0f8e-4d7c-9b6a-5e4d3c2b1a09" \
  -H "Authorization: Bearer $SENTINEL_KEY"

Reviewing matches

Each match gets a decision from a person, with a note saying why. There are three outcomes:

OutcomeMeaningTakes effect
true_matchIt's the person or business on the listAt once
needs_infoCan't tell yet. The match stays open.At once
false_positiveIt's someone elseOnce a second key approves it

Clearing a match takes two people, the same as closing an alert. Confirming one or asking for more information doesn't, since neither lowers the risk.

While a cleared match waits for approval, nobody can record another decision on it. Approve or reject the pending one first. Otherwise a later decision replaces the earlier one, and both stay in resolutions.

You can also decide on a near miss. Confirming a near miss as a true match sets review to confirmed.

When every match on a screening is cleared and approved, its alert closes by itself with the disposition false_positive. The key that cleared the last match is recorded as having requested closure, and the key that approved it as having closed it. A confirmed true match leaves the alert open for someone to act on. If the alert is already pending closure or closed, it's left as it is.

Each step is written to the audit log as screening.match_resolved, screening.resolution_approved or screening.resolution_rejected, and an alert that closes this way adds alert.closed.

Resolve a match

POST /v1/screenings/{id}/resolutions

Records a decision about one match on the screening.

list
string required
The match's list code, from matches[].list.
entry_ref
string required
The match's entry, from matches[].entry_ref.
outcome
string required
true_match, false_positive or needs_info.
note
string required
Why. 1 to 2,000 characters. It's kept with the decision and in the audit log.
Terminal
curl -X POST "$SENTINEL_URL/v1/screenings/4c3b2a19-0f8e-4d7c-9b6a-5e4d3c2b1a09/resolutions" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "list": "sample",
    "entry_ref": "SMP-0003",
    "outcome": "false_positive",
    "note": "Passport shows a different place and date of birth. Copy on file."
  }'
Response · 201
{
  "id": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
  "list": "sample",
  "entry_ref": "SMP-0003",
  "outcome": "false_positive",
  "note": "Passport shows a different place and date of birth. Copy on file.",
  "status": "pending_approval",
  "resolved_by": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
  "created_at": "2026-10-16T10:52:03.418Z"
}

status is pending_approval for false_positive and final for the other two.

Approve a cleared match

POST /v1/screenings/{id}/resolutions/{resolution_id}/approve

Makes a pending false_positive final. It must be called with a different key from the one that cleared the match, or it returns 403. No body is needed.

Terminal
curl -X POST "$SENTINEL_URL/v1/screenings/4c3b2a19-0f8e-4d7c-9b6a-5e4d3c2b1a09/resolutions/7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d/approve" \
  -H "Authorization: Bearer $SENTINEL_REVIEWER_KEY"
Response · 200
{
  "id": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
  "list": "sample",
  "entry_ref": "SMP-0003",
  "outcome": "false_positive",
  "note": "Passport shows a different place and date of birth. Copy on file.",
  "status": "final",
  "resolved_by": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
  "decided_by": "8e7d6c5b-4a39-4281-b0c1-d2e3f4a5b6c7",
  "created_at": "2026-10-16T10:52:03.418Z",
  "decided_at": "2026-10-16T11:04:40.912Z"
}

If this was the last match left to clear, the screening's review becomes cleared and its alert closes.

Reject a cleared match

POST /v1/screenings/{id}/resolutions/{resolution_id}/reject

Sends a pending false_positive back. Its status becomes rejected, decided_by and decided_at are set, and the match has no standing decision again. Any key can reject, including the one that cleared it, so it also works as "withdraw my decision". No body is needed.

Resolution fields

FieldTypeMeaning
idstring, uuidThe decision's ID
list, entry_refstringThe match it's about
outcomestringtrue_match, false_positive or needs_info
notestringWhy
statusstringpending_approval, final or rejected
resolved_bystringThe key ID that made the decision
decided_bystringThe key ID that approved or rejected it. Absent until then.
created_atstring, date-timeWhen it was made
decided_atstring, date-timeWhen it was approved or rejected. Absent until then.

Loaded lists

GET /v1/screening/lists

Returns the lists screenings currently run against.

Response · 200
{
  "lists": [
    {
      "code": "sample",
      "name": "Sample list (synthetic, for testing only)",
      "version": "2026-10-16",
      "entries": 25,
      "loaded_at": "2026-10-16T09:00:02.114Z"
    }
  ]
}

Not built yet

There's no ongoing re-screening when a list changes. A customer is only checked again when you screen them again. Stored screenings hold personal data, and there's no retention setting for them yet.

The dashboard's Screening screen uses these endpoints: screen a name, look through recent checks, and resolve or approve matches.

Errors

EndpointCodeerror
All401missing or invalid API key
GET /v1/screenings400limit must be between 1 and 200
POST /v1/screenings400name must be 1 to 200 characters
POST /v1/screenings400kind must be person or business
POST /v1/screenings400birth_year looks wrong
POST /v1/screenings400country must be a two-letter code
POST /v1/screenings400threshold must be between 50 and 100
POST /v1/screenings503no screening list is loaded, so nothing can be checked
GET /v1/screenings/{id}404screening not found
POST .../resolutions400outcome must be true_match, false_positive or needs_info
POST .../resolutions400note must be 1 to 2000 characters
POST .../resolutions400this screening has no such match
POST .../resolutions404screening not found
POST .../resolutions409a decision on this match is waiting for a second person
POST .../approve, .../reject404resolution not found
POST .../approve403the person who cleared a match can't approve it
POST .../approve409only a cleared match waiting for approval can be approved
POST .../reject409only a cleared match waiting for approval can be sent back
Sentinel is a Somu Labs product. © 2026 Somu Labs Ltd.