Aller au contenu

Screening API

Ce contenu n’est pas encore disponible dans votre langue.

The Screening API is what the Screening page and Settings → Screening use, plus several things that have no page in the app yet: screening a name on demand, setting up providers and importing lists. Every endpoint is for signed-in people: an API key gets 401 sign in required. For what each feature does, read the guides under Sanctions and PEP screening first.

Permission Allows
screening.view Every GET below, and POST /screening/screen against the lists (without "providers": true)
screening.manage Everything screening.view allows, plus every change: settings, providers, re-screens, list and bundle imports. Also POST /screening/subjects/{type}/{id}, and POST /screening/screen with "providers": true
settings.view Only the GET endpoints for settings, sources, versions, runs and providers
screening.review POST /screening/hits/{id}/assign, /comments and /resolve; also enough for GET /screening/hit-filters. Reading the matches themselves needs screening.view
screening.approve Approving a screening change request, including a decision on a potential match (through the Change requests API)
screening.accept_removal accept_removal=true on POST /screening/bundles, together with screening.manage
Endpoint What it does Answer
GET /api/v1/screening/settings Your workspace’s screening settings 200
PUT /api/v1/screening/settings Change them (may need approval) 200, or 202
GET /api/v1/screening/sources The installation’s lists and your workspace’s own, each with its current version and whether you screen against it 200
GET /api/v1/screening/sources/{key}/versions A list’s versions, newest first 200
POST /api/v1/screening/sources/{key}/import Import a file as one of your workspace’s own lists, import:<name> (may need approval) 200, or 202
POST /api/v1/screening/bundles Import a signed list bundle (self-hosted only; may need approval) 200, or 202
POST /api/v1/screening/rescreen Queue a full re-screen 202
GET /api/v1/screening/runs Recent re-screens 200
POST /api/v1/screening/screen Screen a name on demand, storing nothing 200
POST /api/v1/screening/subjects/{type}/{id} Screen one customer or counterparty now and record the result 200
GET /api/v1/screening/hits Potential matches 200
GET /api/v1/screening/hits/{id} One potential match, with your record, the list entry and the timeline 200
GET /api/v1/screening/hit-filters What the queue can be filtered by, and who a match can be assigned to 200
POST /api/v1/screening/hits/{id}/assign Assign an open match, or take it back 204
POST /api/v1/screening/hits/{id}/comments Add a comment to a match’s timeline 204
POST /api/v1/screening/hits/{id}/resolve Resolve an open match as a true match or a false positive (may need approval) 200, or 202
GET /api/v1/screening/providers Configured providers and the provider catalogue 200
PUT /api/v1/screening/providers Add or change a provider (may need approval) 200, or 202
DELETE /api/v1/screening/providers/{id} Remove a provider (may need approval) 204, or 202
Status Meaning Examples
401 Not signed in, or called with an API key sign in required
403 Missing permission, a list that isn’t yours to change, or not allowed on this installation missing permission screening.manage; asking the screening providers sends the name to a third party: missing permission screening.manage; not allowed: ng-nigsac is one of the installation's lists, the same for every workspace: …; not allowed: the official lists are managed by SpeculaPro on the hosted service
404 No such potential match, provider, customer or counterparty in your workspace not found
409 No list is loaded yet, or the match is not in a state that allows the action no list has been loaded yet: the worker fetches the official lists, or import one; the match is already cleared; conflict: the match is superseded; only open matches are assigned; conflict: the customer this match is about no longer exists
413 The body is larger than the endpoint allows: 2 MiB for a list import, SP_INGEST_MAX_BODY_BYTES for a bundle, SP_HTTP_MAX_BODY_BYTES elsewhere request body too large
422 A field fails its rules, or the request can’t be carried out as things stand invalid: the threshold must be between 0.50 and 0.99; invalid: unknown list source "x"; invalid: use an https base URL; invalid: World-Check One needs a group id; invalid: an imported list's key is import:<name> (2 to 57 characters: a-z, 0-9, - _ .); invalid: a workspace keeps at most 20 lists of its own; invalid: screening is turned off for this workspace; turn it on to re-screen; not a list in the expected format: the header needs key and name columns
428 Approval is required and the request has no X-Change-Reason header. See Approvals
202 The change is waiting for a second person’s approval; the body is the change request

These endpoints can need a second person’s approval, under the action type Screening sources and thresholds (screening.config) of the workspace’s approval policy. It is on by default once two active people hold screening.approve.

Endpoint Change request operation
PUT /api/v1/screening/settings screening.settings
PUT /api/v1/screening/providers screening.provider_save
DELETE /api/v1/screening/providers/{id} screening.provider_delete
POST /api/v1/screening/sources/{key}/import screening.import_list
POST /api/v1/screening/bundles screening.import_bundle

When approval applies, a request without the X-Change-Reason header is answered 428. With it, the answer is 202 and the change request; nothing changes until someone with screening.approve approves it. See Change requests API.

A request is checked before it is proposed: a missing permission, a key your workspace may not import under, a file that doesn’t parse, or a bundle whose signature doesn’t verify is answered 403 or 422, and no change request is made.

If what a request depends on changed before it was approved, it is closed as stale: for a list import, the list got another version; for a bundle, one of the lists it carries did.

A decision on a potential match has two action types of its own:

Endpoint Decision Action type Operation
POST /api/v1/screening/hits/{id}/resolve A true match, or clearing a match against a sanctions list screening.disposition (on by default) screening_hit.resolve
POST /api/v1/screening/hits/{id}/resolve Clearing a PEP, adverse-media or watch-list match screening.clearance (off by default) screening_hit.clear

The reason in the request body serves as the change reason, so no header is needed. See Resolve a potential match.

POST /screening/rescreen, POST /screening/screen, POST /screening/subjects/{type}/{id}, and assigning or commenting on a match, never go through approval.

{
"enabled": true,
"threshold": 0.85,
"screen_customers": true,
"screen_counterparties": true,
"sources": ["eu-fsf", "gb-uksl", "un-sc", "us-ofac-cons", "us-ofac-sdn"],
"default_sources": true
}
Field Meaning
enabled Screening on. Off: the worker does no automatic screening for the workspace
threshold Score from which a candidate is a potential match, 0.5 to 0.99
screen_customers, screen_counterparties Which subjects are screened
sources Keys of the lists the workspace screens against
default_sources Present and true when no choice has been saved. sources is then the lists of the workspace’s jurisdiction (see Default lists by jurisdiction) and follows it until a choice is saved

Send the whole object (without default_sources). The answer is the saved settings.

  • threshold outside 0.5 to 0.99 is 422. So is a sources key that is neither one of the installation’s lists nor one of your workspace’s own.
  • Changing the threshold or the sources, or turning screening on, queues a full re-screen (unless one is already waiting or running).
  • Each fetched list in sources starts being fetched for the installation if it wasn’t.
  • Open potential matches on a list that is no longer in sources are closed as superseded, with closed_reason source_removed. Provider matches are left open.
  • Audited as screening.settings, with the settings before and after, and as screening.hits.close when matches were closed.

An array, one object per list: every list of the installation, and your workspace’s own. Another workspace’s own lists are never in it.

Field Meaning
key, name, publisher, jurisdiction The list. jurisdiction is a country code, UN, EU or UEMOA
format The publisher’s format (un-xml, ofac-xml, eu-fsf-xml, uk-xml, fic-xml, sema-xml, dfat-xlsx, ke-xlsx), import or provider
url Where a feed is fetched from, including an address the operator set
origin builtin, import (an imported list, the installation’s or your own) or provider
workspace true: your workspace’s own imported list, which no other workspace sees. false: one of the installation’s lists
feed true: fetched on a schedule. false: imported
installed The installation keeps this list (a feed that is off is not fetched)
enabled This workspace screens against it
interval_seconds How often a feed is checked
last_fetch_at, last_fetch_status, last_error The last fetch: ok (a new version), unchanged, older (the publisher’s file was older than the version held, which stays; last_error carries a note saying so) or error. A fetch refused because it would remove too much of the list is also error, with the counts in last_error
next_fetch_at When a feed that is on is next due
notes Notes about the publisher
version The current version, absent when the list has never been loaded. Its added, changed and removed are always 0 here; read them from the versions endpoint

GET /api/v1/screening/sources/{key}/versions

Section titled “GET /api/v1/screening/sources/{key}/versions”

limit is 1 to 200 (default 50). Newest first.

[
{
"id": "0199f1c2-…",
"source": "un-sc",
"seq": 4,
"published_at": "2026-10-09T00:00:00Z",
"fetched_at": "2026-10-09T06:00:12Z",
"file_sha256": "9f2c…",
"content_sha256": "41ab…",
"entries": 1012,
"added": 3,
"changed": 1,
"removed": 0,
"origin": "fetch"
}
]
Field Meaning
seq The version number for this list: 1, 2, 3 …
published_at The publisher’s date, when the file gives one
fetched_at When it was fetched or imported
file_sha256 SHA-256 of the file as published
content_sha256 SHA-256 of the parsed entries. Two files with the same entries have the same value, and no new version is stored
entries, added, changed, removed The size of the list and what changed since the previous version
origin fetch, import, bundle or provider_sync
bundle_key_id For a bundle, the key that signed it

The versions are those of the list as your workspace sees it: your own list by that key if you have one, otherwise the installation’s. A key that is neither answers 200 with an empty array.

POST /api/v1/screening/sources/{key}/import

Stores a file as a new version of one of your workspace’s own lists. Only your workspace sees the list and screens against it. It works on self-hosted installations and on the hosted service. Needs screening.manage. The app’s Import a list button under Settings → Screening calls this endpoint; it sends no X-Change-Reason, so use the API directly when the import needs approval.

The body is the file, in the import format, with Content-Type: text/csv or application/xml, up to 2 MiB. {key} is import:<name>, where <name> is 2 to 57 characters: a-z and 0-9, and after the first character also -, _ and .. The optional query parameter name (up to 120 characters) is the list’s display name: it names a new list, and renames an existing one when sent again.

{"source": "import:internal-watchlist", "version_id": "0199…", "seq": 3, "unchanged": false,
"entries": 214, "added": 2, "changed": 1, "removed": 0}
Field Meaning
source The list’s key
version_id, seq The version stored. With unchanged or older, the current version, which stays
unchanged true: the entries are the same as the current version, and nothing was stored
older Present and true when the file was published before the current version, so nothing was stored. Only a fetch or a signed bundle is compared this way; an import through this endpoint never answers it
entries Entries in the file
added, changed, removed What changed since the previous version
accepted_removal Present and true on a version stored although it removed more than a list may lose by itself: by the operator with --accept-removal, or by a bundle import with accept_removal=true

The checks, in order. Everything down to the file check happens before any change request is made:

Check Answer
The body is larger than 2 MiB 413
{key} is a built-in list (ng-nigsac, gh-domestic, uemoa-gel, un-sc …) 403, not allowed: … is one of the installation’s lists, the same for every workspace: the operator imports it (speculapro screening import-list, or a signed list bundle). A workspace imports its own lists as import:<name>
{key} is not a valid import:<name> 422, invalid: an imported list’s key is import:<name> (2 to 57 characters: a-z, 0-9, - _ .)
{key} is an import:<name> list of the installation 403, not allowed: … is one of the installation’s lists; give your own list another name
The list would be your workspace’s twenty-first 422, invalid: a workspace keeps at most 20 lists of its own
The file doesn’t parse 422, for example not a list in the expected format: the header needs key and name columns
Approval applies and there is no X-Change-Reason 428
Approval applies 202, with the change request. Its summary shows the list, the size in bytes and the first 16 characters of the file’s SHA-256
  • The 2 MiB limit is this endpoint’s own. A smaller SP_HTTP_MAX_BODY_BYTES does not shorten it.
  • The new version is audited as screening.list.version, in your workspace.
  • Importing does not add the list to the settings’ sources. Add it with PUT /screening/settings to screen against it.

The installation’s lists that have no feed (ng-nigsac, gh-domestic, uemoa-gel) are imported by the operator with speculapro screening import-list. See Screening lists for operators.

POST /api/v1/screening/bundles

Imports a bundle of lists signed by SpeculaPro into the installation’s lists, for every workspace. Self-hosted installations only. Needs screening.manage.

The body is the bundle file, sent as Content-Type: application/json or application/octet-stream. The signature is checked before anything in the file is read.

What happens, in order:

  1. On the hosted service the request is refused: 403, not allowed: the official lists are managed by SpeculaPro on the hosted service.
  2. The signature is checked, and each list’s key. A bundle that doesn’t verify is 422, invalid: list bundle is not valid: …. So is a bundle with no list, one that carries a list twice, or one that carries an import:<name> key a workspace uses for its own list.
  3. If approval applies (screening.config): without X-Change-Reason the answer is 428. With it, the file is kept on the server and the answer is 202 with the change request. The request carries the bundle’s SHA-256, the id of the key that signed it, its creation time and the lists it contains with their entry counts and publication dates, not the file itself. The approver approves it under Approvals as usual.
  4. If approval does not apply, the bundle is imported at once and the answer is 200.

On approval the kept file is read back, checked against the SHA-256 in the request, verified again and imported; the kept file is then removed. A kept file that nobody approved is removed after 8 days. If it is gone when the request is approved, the approval fails with the uploaded bundle is no longer kept; send it again. If one of the bundle’s lists got a newer version since the proposal, the request is closed as stale.

The 200 answer is an array with one result per list in the bundle, in the same shape as an import:

[
{"source": "un-sc", "version_id": "0199…", "seq": 5, "unchanged": false,
"entries": 1012, "added": 2, "changed": 1, "removed": 0},
{"source": "us-ofac-sdn", "version_id": "0199…", "seq": 9, "unchanged": false, "older": true,
"entries": 17804, "added": 0, "changed": 0, "removed": 0}
]
  • All or nothing. Every list in the bundle is stored in one transaction. If one fails, none is stored.
  • "older": true: the installation already holds a version of that list published later, and it stays. See A list never goes back in time.
  • "unchanged": true: the list’s entries are the ones already held.
  • A list that would lose many entries is refused. If a list in the bundle would remove more than 20% of a current version of 50 entries or more (the operator’s settings), the import answers 422, invalid: large removal refused: the new … would remove … of the current version’s … entries …, and no list in the bundle is stored. When approval applies, the refusal comes when the request is approved.
  • Accepting a large removal. After checking with the publisher that the entries really were removed, send the same request with ?accept_removal=true. It needs the permission screening.accept_removal as well as screening.manage (without it: 403, missing permission screening.accept_removal). When approval applies, the change request’s summary says ACCEPTING LARGE REMOVALS so the approver sees it. Each list accepted this way is audited as screening.bundle.accept_removal, with the list, the version, how many entries were removed and the bundle’s SHA-256. The operator can do the same from the command line with speculapro screening import-bundle <file> --accept-removal. See When a list loses many entries.
  • The body limit is SP_INGEST_MAX_BODY_BYTES (8 MiB by default). The bundle format itself allows 96 MiB. For a larger bundle raise that setting or use the command line, speculapro screening import-bundle <file>: see Screening lists for operators.
  • Each stored version is audited as screening.list.version, in the workspace of the person who proposed the import.

No body. Answers 202:

{"id": "0199f1d0-…", "queued": true}

"queued": false means a full re-screen was already waiting or running; its id is returned and nothing new was queued. Audited as screening.rescreen when one is queued.

While screening is turned off for the workspace the request is refused: 422, invalid: screening is turned off for this workspace; turn it on to re-screen.

limit is 1 to 200 (default 50). Newest first.

Field Meaning
kind full, or delta (a list change)
trigger manual, settings or list_change
source, from_seq, to_seq For a delta: the list, and the versions applied (after from_seq, up to to_seq)
status queued, running, done or failed
subjects, hits People screened, and the potential matches they have
error Why a failed run stopped. A queued full re-screen of a workspace that turned screening off before it ran ends as failed with not run: screening is turned off for this workspace
requested_by The person who started a manual run
created_at, started_at, finished_at Times

POST /api/v1/screening/screen

Screens a name against the workspace’s lists, and its providers if asked. The name and the result are not stored.

  • Against the lists it needs screening.view or screening.manage.
  • With "providers": true it needs screening.manage, because the name goes to a third party that may bill the call. Without it the answer is 403, asking the screening providers sends the name to a third party: missing permission screening.manage.

Every call is written to the audit log as screening.screen_name, before any provider is called. If the entry can’t be written, nothing is sent. The entry never holds the name:

Audit field Meaning
name_hmac A keyed fingerprint of the name (an HMAC with your workspace’s key, over the name in lower case with single spaces). The same name gives the same value in your workspace; the name can’t be worked back from it without the key
kind The kind sent
with_date_of_birth, with_id_number Whether a date of birth and an ID number were given, not their values
countries, threshold As screened
lists The lists screened
providers The providers the name was sent to; empty when none were asked
{
"name": "Mamadou Ousmane",
"kind": "person",
"date_of_birth": "1975",
"countries": ["NG"],
"providers": false
}
Field Rules Meaning
name Required, 2 to 200 characters The name to screen
kind person, organization, vessel or empty Empty: compared as unknown
date_of_birth YYYY, YYYY-MM or YYYY-MM-DD
countries Up to 5 two-letter codes Nationality or country
id_number Up to 64 characters Compared with list ID numbers of every type
threshold 0 to 0.99 Defaults to the workspace threshold (also when 0)
limit Up to 50 Most list results; default 20
providers Needs screening.manage true: also ask the workspace’s providers. Your provider may bill for it
{
"matches": [
{
"source": "un-sc",
"entry_key": "QDi.000",
"entry_name": "Muhammad Usman",
"entry_kind": "person",
"categories": ["sanction"],
"list_version": {"id": "0199f1c2-…", "source": "un-sc", "seq": 4},
"score": 0.95,
"explanation": {
"score": 0.95,
"name_score": 1,
"method": "jaro_winkler_token_set",
"query_name": "Mamadou Ousmane",
"matched_name": "Muhammad Usman",
"tokens": [
{"query": "muhammad", "entry": "muhammad", "sim": 1, "how": "variant"},
{"query": "usman", "entry": "usman", "sim": 1, "how": "variant"}
],
"factors": [
{"code": "dob_year", "effect": 0.02, "detail": "DOB year match"},
{"code": "country_mismatch", "effect": -0.07, "detail": "nationality/country mismatch (NG vs PK)"}
],
"text": "name 1.00 (Jaro-Winkler token set; muhammad≈muhammad variant; usman≈usman variant), DOB year match, nationality/country mismatch (NG vs PK)"
}
}
],
"threshold": 0.85,
"sources": ["eu-fsf", "gb-uksl", "un-sc", "us-ofac-cons", "us-ofac-sdn"],
"took_ms": 3.4
}

The entry above is made up to show the shape; the numbers are illustrative.

Field Meaning
matches Results at or above the threshold, best first
sources The lists that were actually screened: your lists that have a version loaded
took_ms How long the search took on the server
errors Present when a provider could not be reached, one line per provider

How to read explanation: How matching works.

409 when none of your lists has a version loaded and you didn’t ask for providers.

POST /api/v1/screening/subjects/{type}/{id}

{type} is customer or counterparty; {id} is its id. Screens it against the workspace’s lists and providers, whether or not anything changed, and records the potential matches.

Needs screening.manage: the call writes potential matches and calls your providers, which may bill each search. screening.view alone is answered 403.

{"subject_type": "customer", "subject_id": "0199e0aa-…", "hits": 1, "incomplete": false}

When a provider could not be asked:

{"subject_type": "customer", "subject_id": "0199e0aa-…", "hits": 1, "incomplete": true,
"errors": ["opensanctions: the provider is unavailable"]}
Field Meaning
hits The number of open potential matches found
incomplete true: a provider could not be asked. The subject is screened against the lists only for now, that provider’s earlier matches on it stay open, and the worker retries the provider. See Providers
errors Present with incomplete: one line per provider that could not be asked, as provider: error

404 when there is no such subject. 409 when no list is loaded yet.

Audited as screening.screen_subject on the customer or counterparty, with the lists screened, the providers asked, the number of hits and whether the screening was incomplete.

Parameter Meaning
status open, superseded, delisted, cleared or confirmed. Empty: all
subject_id Only this customer’s or counterparty’s
source Only matches against this list or provider: a list key, or provider:<key>
subject_type customer or counterparty
category sanction, pep, pep_rca, adverse_media or watchlist
assignee me, unassigned, or a person’s id. Anything else is 422, assignee is me, unassigned or a person’s id
before Paging: the created_at of the last potential match of the previous page
before_id Paging, with before: the id of that last potential match
limit 1 to 500 (default 100)

Newest first, ordered by created_at and then id. To page, pass both values of the last one you received:

Terminal window
curl "https://speculapro.example/api/v1/screening/hits?status=open&limit=500&before=2026-10-09T06:00:12.431Z&before_id=0199f1c2-7a10-7c3e-9d0e-2f4b6a8c1e55" -b cookies.txt

Stop when a page comes back with fewer than limit.

Field Meaning
id The potential match
subject_type, subject_id, subject_name Who was screened, and the name as screened
source The list key (one of the installation’s lists or one of your own), or provider:<key>
entry_key, entry_name, entry_kind The list entry: its reference, the name or alias that matched, and its kind
entry_revision A fingerprint of the entry’s content when it matched
list_version_id The list version it was found in (absent for a provider)
categories sanction, pep, pep_rca, adverse_media, watchlist, other
score, name_score, threshold The final score, the name score, and the threshold in force
explanation See How matching works
status See Statuses
closed_reason Why it left the queue without a person’s decision: no_longer_matches (a re-screen no longer found it), entry_delisted (the entry left its list) or source_removed (the workspace stopped screening against the list). Absent on an open match
closed_at When that happened. Absent on an open match
created_at, updated_at When it was first found, and last refreshed
assignee_id, assignee_name The reviewer it is assigned to, if anyone
decided_by, decided_at Who resolved it, and when. Absent on a match nobody decided
case_id The case a confirmed match opened or joined
pending_approval true while a decision on it waits for a second person. The match is still open
deciding Only on GET /screening/hits/{id}, for an open match: who decides each outcome in this workspace now, as {"true_match": …, "false_positive": …}. Each is reviewer (anyone with screening.review, alone), second_person (a reviewer proposes and someone with screening.approve approves) or approver (only someone with screening.approve, alone)

The same fields, plus:

Field Meaning
subject Your record as it is now: name, kind, date_of_birth (customers, and only if you hold entities.view_pii), countries and id_type (the kind of identity document; the number is never returned here). Absent once the record is gone
entry The list entry as it stands now: key, kind, names, dobs, nationalities, ids, programs, listed_on, remarks. Absent when the entry has left the list, and for a provider match
provider_detail For a provider match, the provider’s own answer
designation_id The designation a confirmed match set, or the one already in force
decided_by_name, decision_note Who decided, and the reason they gave
case_number The number of the case, such as SP-2026-000123
pending_request The change request carrying a decision that waits for approval
events The timeline, oldest first. Each has id, type (comment, assigned, unassigned, confirmed, cleared), body (the comment or the decision’s reason), payload, actor_id, actor_name and created_at

For anyone with screening.view, screening.review or screening.manage.

{
"sources": ["us-ofac-sdn", "import:demo-watch", "provider:opensanctions"],
"reviewers": [{"id": "0199e0a1-…", "name": "Abena Owusu"}]
}

sources are the lists and providers your workspace has matches on. reviewers are the active people who hold screening.review: the only people a match can be assigned to.

Needs screening.review. Answers 204.

{"assignee_id": "0199e0a1-…"}

Send null, or leave assignee_id out, to take the match back from whoever has it. Only an open match can be assigned (409 otherwise), and only to an active person with screening.review (422, the assignee is not an active person who may review matches in this workspace). Assigning to the person who already has it changes nothing. Audited as screening.hit.assign.

Assigning a match to someone else queues a notification for them (kind screening_match_assigned) in the same transaction, sent by email, text message or WhatsApp as they chose. It names the person who assigned it and links to /screening/{id}; it carries no name, list or score. Assigning a match to yourself, or taking it back, notifies nobody. See Notifications. Nobody is notified.

Needs screening.review. Answers 204.

{"body": "Ghana Card checked: born 1996. The list entry gives 1988."}

1 to 4,000 characters, on a match in any status. Audited as screening.hit.comment with the comment’s length; the text itself is kept only on the timeline, where data protection can remove it.

Needs screening.review, and in one case screening.approve as well: see Approval below.

{"outcome": "false_positive", "reason": "Customer born 1996 (Ghana Card); the list entry gives 1988."}
Field Meaning
outcome true_match: the subject is the listed party. false_positive: it is not
reason Required, 3 to 2,000 characters. Kept with the match as evidence, and read by the approver

The answer says what the decision did:

{"status": "confirmed", "designation_id": "0199f3…", "case_id": "0199f3…"}
Field Meaning
status cleared or confirmed
good_guy_id False positive: the good-guy entry recorded. The same subject and list entry are not raised again until the entry’s content or the subject’s screened data changes
designation_id True match: the designation set (sanctioned, pep, pep_rca, adverse_media or watchlist, the gravest of the match’s categories), or the one already in force
case_id True match: the case opened or joined. A sanctions match raises a sanctions alert of critical severity; any other an aml alert of high severity, with the rule key screening.match

Approval:

  • A true_match, and a false_positive on a match whose categories include sanction, fall under the action type screening.disposition (on by default).
  • A false_positive on any other match falls under screening.clearance (off by default).
  • When approval applies, the answer is 202 with the change request, and the match stays open with pending_approval: true until someone with screening.approve approves it. The reason in the body is the maker’s reason: no X-Change-Reason header is needed.
  • While approval for screening.disposition is not in effect (fewer than two active people hold screening.approve, or the workspace switched it off), a decision either way on a match whose categories include sanction needs screening.approve itself. Without it the answer is 403: A match against a sanctions list is decided by someone with screening.approve in this workspace: approval by a second person (screening.disposition) is not in effect, so a reviewer cannot confirm or clear it alone. You can still assign it and comment on it. With it, the decision applies at once (200). The match’s deciding field says which case applies before you call.
  • The request is closed as stale if, before it is approved, the match’s status, score or entry_revision changed.
  • The decision is applied under the maker’s name: decided_by is the reviewer. The approver is on the change request.

Errors: 409 the match is already … for a match that isn’t open; 409 when the customer or counterparty of a true match no longer exists; 422 for a missing or short reason or an unknown outcome.

Audited as screening.hit.clear or screening.hit.confirm, with the list, the entry, the categories, the reason and the ids of what was created. See Reviewing a potential match.

The app’s Connect a provider, Edit and remove buttons under Settings → Screening call these endpoints. See How to connect a provider.

{
"providers": [
{
"id": "0199f200-…",
"provider": "opensanctions",
"name": "OpenSanctions (yente)",
"status": "implemented",
"installation": false,
"config": {"base_url": "https://api.opensanctions.org", "dataset": "default"},
"has_credentials": true,
"enabled": true,
"sync_enabled": false,
"updated_at": "2026-10-10T09:12:00Z"
}
],
"catalog": [
{"key": "opensanctions", "name": "OpenSanctions (yente)", "default_base_url": "https://api.opensanctions.org",
"status": "implemented", "sync": true, "credentials": ["api_key"], "notes": "…"}
]
}

providers are the workspace’s configurations and any for the whole installation ("installation": true, read-only). catalog is every provider SpeculaPro knows, with the credential fields each needs.

Field Meaning
status implemented, or skeleton: not available, never called
has_credentials Whether credentials are stored. They are never returned
last_used_at, last_error The last call, and its error if it failed
sync_enabled true on an installation-wide provider whose data feed is loaded into the list provider:<key>. Always false for a workspace’s own provider
config.data_url On an installation-wide OpenSanctions provider: the bulk data address the operator set. A workspace’s own configuration has none
{
"provider": "worldcheck",
"enabled": true,
"config": {"base_url": "", "group_id": "<your group id>"},
"credentials": {"api_key": "<key>", "api_secret": "<secret>"}
}
Field Meaning
provider opensanctions, complyadvantage, worldcheck, dowjones, bridger or moodys_grid
enabled Whether it is called
config.base_url Empty for the provider’s default address. Must be https and not an internal address (see Network rules). Up to 500 characters
config.dataset OpenSanctions dataset; default when empty
config.group_id World-Check One group; required for worldcheck
config.data_url Leave out. It belongs to the installation-wide data feed, which the operator sets; sending one is 422, invalid: a data URL is for the installation-wide provider feed, which the operator sets
credentials api_key, api_secret, username, password: the ones the catalogue lists for the provider. Leave credentials out to keep the stored ones

Answers 200 with {"id": "…"}. A workspace has one configuration per provider; saving again replaces it. Audited as screening.provider.save, recording the provider, the configuration, whether it is enabled and whether credentials changed, never the credentials.

Required credentials are not checked when you save. A provider saved without them shows as on, and is skipped with a logged warning when it is used. Check with Screen a name and "providers": true.

Answers 204. 404 for a provider that isn’t your workspace’s (including an installation-wide one, which the operator removes with speculapro screening provider remove). Audited as screening.provider.delete.