Screening lists for operators
Ce contenu n’est pas encore disponible dans votre langue.
In short
Section titled “In short”Sanctions and PEP screening is on by default and needs no set-up on an installation with internet access: the worker fetches the official lists and screens every workspace.
As the operator you are responsible for five things:
- A worker is running. Without one, no list is fetched and nobody is screened.
- The worker can reach the list publishers over HTTPS, or the installation is set to air-gapped and you load lists from signed bundles.
- Processes that run the
api,workerordecisionrole have memory for the list index. - Someone watches for fetch failures and provider errors.
- The installation’s lists that have no feed are loaded by you. Nigeria’s NIGSAC list, Ghana’s domestic designations and the UEMOA asset-freezing lists are imported from the command line. A workspace can’t do it. See Installation lists from a file.
What runs where
Section titled “What runs where”| Role | What it does for screening |
|---|---|
worker |
Fetches lists that are due, loads installation-wide provider feeds, applies list changes to each workspace, screens new and changed customers and counterparties, retries provider calls that failed, runs queued re-screens, and calls providers for new and changed subjects |
api |
Serves Settings → Screening and the Screening API. Screening a name or one subject on demand runs here, including any provider call it makes |
| decision | Screens the name of a counterparty that has no stored screening result (the first payment to them) inside the real-time decision, against the lists only, and writes any potential match for review after the answer. It never calls a provider. See Inline screening in the decision role |
All three keep an in-memory index of every current entry of every list the installation keeps. Each
process builds its own from the database, and rebuilds it when a list version changes: the worker at
each pass, an api or decision process within about a minute. A workspace’s own lists are indexed separately,
per workspace, the first time that workspace is screened.
Inline screening in the decision role
Section titled “Inline screening in the decision role”A decision reads a counterparty’s screening state from what the worker stored. The first payment
to a counterparty has nothing stored: its row is written after the decision, and the worker
screens it some seconds later. So a decision process screens that counterparty’s name itself.
- When. Only when the counterparty has no row in the worker’s screening state and no match on record, and the payment carries a name. A match a reviewer cleared is therefore never raised again by a decision.
- How. A search of the in-memory index, with the workspace’s lists and threshold, the same query the worker makes. No database query and no network call. A name already searched is remembered per workspace (up to 50,000 names per process; when full, the memory is emptied and names are searched again), until the workspace’s lists, their versions or its threshold change.
- What it loads. The index is built when the process starts; if that fails, an error is
logged and it is built with the first decision instead. Each workspace’s screening settings and
its own lists are loaded before its first decision and refreshed in the background with its
confirmed sanctions matches (at half of
SP_DECISION_STATE_TTL). List versions are checked at most once a minute. - What it writes. A potential match is handed to a queue in the process (4,096 places) and
written just after the answer, through the worker’s own code: the counterparty’s row if the
persistrole hasn’t written it yet, then one open match per list entry. A unique index makes that exactly once, however many decisions, decision processes and worker passes find it. - What it leaves to the worker. The counterparty’s screening state is not marked as screened, so the worker still screens it in full, providers included.
- When it can’t. If the settings or lists can’t be refreshed, decisions go on with what was loaded before and one error is logged per outage: decision: refresh screening lists and settings; counterparties without a stored result are screened against what was loaded before. If a match can’t be written, or the queue is full, the decision is unaffected, the failure is counted (see Metrics) and the worker records the match when it screens the counterparty. On shutdown the queue is written out before the process exits.
- Degraded decisions. A decision that can’t finish its checks in time doesn’t apply an inline match, as it doesn’t apply a stored open one. Only confirmed sanctions matches are applied then. See Real-time decisions.
There is no setting for it beyond SP_SCREENING_ENABLED. With that false, decision processes
load no index and screen nothing inline.
The worker’s pass
Section titled “The worker’s pass”Every SP_SCREENING_INTERVAL (30 seconds by default), and straight away when customers are
ingested by the same process, the worker:
- fetches each feed whose time has come (at most 20 per pass);
- every six hours, loads the data feed of each installation-wide provider that has one switched on;
- rebuilds the list index if a version changed;
- puts back in the queue any re-screen that has been running for more than two hours;
- removes signed bundles that were sent through the API and have waited more than 8 days for an approval that never came;
- for each workspace with screening on:
- closes open potential matches on lists the workspace no longer screens against (checked when its lists change, and once after a start);
- applies list changes;
- screens customers and counterparties whose record was written or changed since its last pass;
- asks providers again about the subjects whose earlier call failed and whose retry is due;
- runs queued full re-screens, one after another. A re-screen queued by a workspace that has since turned screening off is not run: it ends as failed with not run: screening is turned off for this workspace.
Counterparties are found by when their record was written, or its name, country or type changed, not by the date of the transaction they first appeared in. A counterparty first seen in a late or back-dated transaction is screened like any other.
Running more than one worker is safe. A pass takes a PostgreSQL advisory lock; a worker that doesn’t get it skips the pass. So screening is done by one worker at a time, and another takes over if it stops.
A workspace with many customers takes a while on its first pass, because everyone is screened once. Later passes only handle what changed.
Outbound access
Section titled “Outbound access”On an installation that is not air-gapped, worker processes need outbound HTTPS (port 443) to the publishers of the lists your workspaces use:
| List | Host |
|---|---|
| UN | scsanctions.un.org (redirects to a storage address that changes) |
| OFAC | sanctionslistservice.ofac.treas.gov (redirects to a storage address that changes) |
| EU | webgate.ec.europa.eu |
| UK | sanctionslist.fcdo.gov.uk |
| South Africa | tfs.fic.gov.za |
| Canada | www.international.gc.ca |
| Australia | www.dfat.gov.au |
| Kenya | www.frc.go.ke |
Because the UN and OFAC redirect to short-lived addresses, an allow-list of fixed host names is
fragile. Redirects are followed up to five times, and only to https addresses.
If a workspace uses a provider, worker and api processes also need to
reach it: api.opensanctions.org, api.complyadvantage.com (or a regional host),
api-worldcheck.refinitiv.com, or your own yente server.
Fetches and provider calls do not use a proxy (HTTPS_PROXY is ignored), so that the
destination check below sees the real address. The processes need a direct route.
With the Helm chart
Section titled “With the Helm chart”The settings below are the chart’s screening values (enabled, airGapped, allowPrivate,
fetchTimeout, interval, batchSize, and sourceUrls as a map of list key to address).
Screening has its own network policy rule, networkPolicy.egress.screening, for worker pods
(list fetches and provider calls) and api pods (on-demand provider calls). It no longer depends
on the webhook rule: setting networkPolicy.egress.webhookPorts to [] does not stop list fetches.
| Installation | What the rule allows |
|---|---|
| Default | Port 443 to the public internet, without the private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, 169.254.0.0/16, fc00::/7, fe80::/10) |
screening.airGapped: true |
Nothing, unless you set to |
screening.enabled: false |
Nothing; the rule is left out |
-
toreplaces the default destinations. For a yente server or list mirror on your own network, setscreening.allowPrivate: trueand name it underto; to keep the publishers as well, list the public block next to it:screening:allowPrivate: truenetworkPolicy:egress:screening:ports: [443, 8000]to:- ipBlock: {cidr: 0.0.0.0/0, except: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, 169.254.0.0/16]}- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: yente}} -
A port applies to every destination in
to. If that is too wide, leaveportsat[443]and give the other server its own rule undernetworkPolicy.egress.extra. -
Network policies add up. While
networkPolicy.egress.webhookPortsis[443], worker pods can already reach any address on 443, private ones included; the same goes for api pods andnetworkPolicy.egress.sso. Narrow or empty those if screening must be the only way out. -
The chart refuses to render
screening.allowPrivate: truewithdeployment: saas, and aSP_SCREENING_*setting underconfig(use thescreeningvalues, which the rule follows).
See Kubernetes with Helm.
Destination checks
Section titled “Destination checks”Every connection for a list fetch or a provider call is checked when it is made, after the name is
resolved and for every redirect. Private, loopback, link-local and cloud metadata addresses are
refused. SP_SCREENING_ALLOW_PRIVATE=true allows private addresses (for a yente server or list
mirror on your own network); link-local and metadata addresses stay refused.
Memory
Section titled “Memory”The list index holds every current entry of every list the installation keeps, in each api,
worker and decision process. A decision process also remembers up to 50,000 screened names
per workspace it decides for. With the built-in sanctions lists this is modest. When a list changes, the old and
new index exist side by side for a moment while the new one is built.
A List change re-screen also builds a temporary index of the workspace’s customers and counterparties in the worker, so worker memory grows with the size of your largest workspace.
SpeculaPro publishes no sizing figures for this yet. Watch worker memory through your first list update, and size from what you see.
Settings
Section titled “Settings”Set these on every process, so api and worker agree.
| Variable | Default | Meaning |
|---|---|---|
SP_SCREENING_ENABLED |
true |
false: the worker does no screening work at all (no fetches, no screening, no re-screens), and decision processes screen no counterparty inline. The API and the settings page still answer from what is stored |
SP_SCREENING_AIR_GAPPED |
false |
true: never fetch a list from its publisher. Lists come from signed bundles and imports only |
SP_SCREENING_ALLOW_PRIVATE |
false |
true: list fetches and providers may reach private network addresses, and a provider address or an address in SP_SCREENING_SOURCE_URLS may be plain http. For self-hosted installations only: with SP_DEPLOYMENT=saas SpeculaPro refuses to start |
SP_SCREENING_FETCH_TIMEOUT |
5m |
The longest one list download may take |
SP_SCREENING_INTERVAL |
30s |
Time between worker passes |
SP_SCREENING_BATCH_SIZE |
1000 |
Customers or counterparties screened per database transaction |
SP_SCREENING_SOURCE_URLS |
(empty) | Your own download address for a built-in list, as space-separated key=url pairs. See When a publisher moves a list |
SP_SCREENING_MAX_REMOVAL_PERCENT |
20 |
A fetched or bundled list that would remove more than this share of the current version’s entries is refused until you accept it. 100 switches the check off. See When a list loses many entries |
SP_SCREENING_MAX_REMOVAL_MIN_ENTRIES |
50 |
Lists whose current version has fewer entries than this are not checked |
All of them are in the configuration reference.
.env.example lists them too, and the Helm chart has a screening value for each (the two removal
settings are screening.maxRemovalPercent and screening.maxRemovalMinEntries).
What stops start-up
Section titled “What stops start-up”The screening settings are checked when a process starts, and when you run a
speculapro screening command. A wrong value stops it with a clear message instead of running with
a setting that doesn’t do what you meant:
| Setting | Refused when | Message |
|---|---|---|
SP_SCREENING_ALLOW_PRIVATE |
true together with SP_DEPLOYMENT=saas |
config: SP_SCREENING_ALLOW_PRIVATE can't be set when SP_DEPLOYMENT=saas |
SP_SCREENING_INTERVAL, SP_SCREENING_FETCH_TIMEOUT |
Either is below 1s |
config: SP_SCREENING_INTERVAL and SP_SCREENING_FETCH_TIMEOUT must be at least 1s |
SP_SCREENING_BATCH_SIZE |
Below 1 or above 10000 |
config: SP_SCREENING_BATCH_SIZE must be between 1 and 10000, got 0 |
SP_SCREENING_MAX_REMOVAL_PERCENT, SP_SCREENING_MAX_REMOVAL_MIN_ENTRIES |
The percentage below 1 or above 100, or the minimum below 1 |
config: SP_SCREENING_MAX_REMOVAL_PERCENT must be between 1 and 100 and SP_SCREENING_MAX_REMOVAL_MIN_ENTRIES at least 1 |
SP_SCREENING_SOURCE_URLS |
A pair is not key=url |
config: SP_SCREENING_SOURCE_URLS: "ke-frc" is not key=url (pairs are separated by spaces) |
SP_SCREENING_SOURCE_URLS |
A key appears twice | config: SP_SCREENING_SOURCE_URLS names ke-frc twice |
SP_SCREENING_SOURCE_URLS |
A key is not a fetched list | config: SP_SCREENING_*: invalid: ng-nigsac is not a fetched list (the fetched lists: un-sc, us-ofac-sdn, …) |
SP_SCREENING_SOURCE_URLS |
An address is not https (or http without SP_SCREENING_ALLOW_PRIVATE=true) |
config: SP_SCREENING_*: invalid: the URL for ke-frc must be https |
SP_SCREENING_SOURCE_URLS |
An address has no host, carries a user name or password, or has a #fragment |
config: SP_SCREENING_*: invalid: the URL for ke-frc is not valid |
SP_SCREENING_SOURCE_URLS |
An address points at a private, internal or metadata host | config: SP_SCREENING_*: invalid: the URL for ke-frc: destination not allowed: … is a private or reserved address |
When a publisher moves a list
Section titled “When a publisher moves a list”Each built-in list is downloaded from an address that ships with your release. Some publishers
change it: Kenya’s FRC publishes every new list under a new dated address, so the built-in one
starts answering 404. You can point a fetched list at its new address without waiting for a
release, in either of two ways:
-
SP_SCREENING_SOURCE_URLS(Helm:screening.sourceUrls), on every process, as space-separatedkey=urlpairs:Terminal window SP_SCREENING_SOURCE_URLS="ke-frc=https://www.frc.go.ke/wp-content/uploads/2026/09/Domestic-List_Kenya.xlsx" -
speculapro screening source set-url <key> <https url>, which stores the address in the database:Terminal window docker compose run --rm cli screening source set-url ke-frc \https://www.frc.go.ke/wp-content/uploads/2026/09/Domestic-List_Kenya.xlsxke-frc is now fetched from https://www.frc.go.ke/wp-content/uploads/2026/09/Domestic-List_Kenya.xlsx (at the worker's next pass)
Which address is used:
| Order | Address | Set with |
|---|---|---|
| 1 | The environment override | SP_SCREENING_SOURCE_URLS |
| 2 | The stored override | speculapro screening source set-url |
| 3 | The built-in address | Your release |
- The keys are the fetched lists:
un-sc,us-ofac-sdn,us-ofac-cons,eu-fsf,gb-uksl,za-fic,ca-sema,au-dfatandke-frc. A list with no feed has no address to set. - Start-up overwrites neither override. The built-in address is refreshed from the release at every start; your override sits beside it and keeps winning.
- An address must be
https(httponly withSP_SCREENING_ALLOW_PRIVATE=true), with no user name or password in it and no#fragment. - It is checked by the destination checks when you set it, and again on every connection the fetch makes, redirects included.
- An invalid address in
SP_SCREENING_SOURCE_URLSstops start-up; see What stops start-up. An invalid address given toset-urlis refused and nothing is stored. - With both set for the same list,
set-urlreminds you: note: a URL for the same list in SP_SCREENING_SOURCE_URLS takes precedence. set-urlandreset-urlmake the list due at once, and are recorded in the audit log asscreening.source.url, by the system.
To go back to the built-in address:
docker compose run --rm cli screening source reset-url ke-frcke-frc is fetched from its built-in URL againTo see every installation list with its origin, current version, last fetch status and the address in use:
docker compose run --rm cli screening source listWhose lists are whose
Section titled “Whose lists are whose”Screening has two kinds of list. As the operator you own the first.
| The installation’s lists | A workspace’s own lists | |
|---|---|---|
| Which | The fetched lists (un-sc, us-ofac-sdn, us-ofac-cons, eu-fsf, gb-uksl, za-fic, ca-sema, au-dfat, ke-frc); the national lists with no feed (ng-nigsac, gh-domestic, uemoa-gel); import:<name> lists you import; provider feeds (provider:<key>) |
import:<name> lists the workspace imported through the API |
| Who sees them | Every workspace | That workspace only: the list, its versions and the matches against it |
| Who writes them | The worker (fetches and provider feeds), your command line, or a signed list bundle | The workspace, with screening.manage |
- A workspace cannot write an installation list. Through the API, an import under
ng-nigsac,gh-domestic,uemoa-gelor any other installation key is refused with403, before any approval request is made. - Two workspaces can use the same key for their own lists. They are different lists.
- You cannot import under a key a workspace already uses for its own list: … is the key of a workspace’s own list; the installation’s list needs another key.
- A workspace keeps at most 20 lists of its own.
- The database enforces it. Row-level security lets the API’s database role read the installation’s lists and the calling workspace’s own, and write only the workspace’s own. The installation’s lists are written by the system role. The one exception is a signed bundle sent through the API, whose signature has been verified first.
What stays self-hosted-only is anything that changes the installation’s lists through the API (a signed bundle) and installation-wide providers. A workspace’s own lists work on the hosted service too.
Which database connection the commands use
Section titled “Which database connection the commands use”Every speculapro screening command except verify-bundle connects with SP_DB_SYSTEM_URL when it
is set, otherwise SP_DB_URL. Run them with the system role’s connection, or the schema
owner’s: the application role is not allowed to write the installation’s lists. With Docker Compose
the cli service already has it.
Installation lists from a file
Section titled “Installation lists from a file”Three official lists are published only as web pages, PDFs or gazette notices, so nothing can fetch them. Someone transcribes the publication into a file in the import format (CSV or XML), and you import it:
docker compose run --rm -v "$PWD/nigsac-2026-06-18.csv:/tmp/nigsac.csv:ro" cli \ screening import-list ng-nigsac /tmp/nigsac.csvng-nigsac version 3: 214 entries (+2, ~1, -0)speculapro screening import-list <key> <file> [--name <display name>]<key> |
Meaning |
|---|---|
ng-nigsac |
Nigeria Sanctions List (NIGSAC) |
gh-domestic |
Ghana domestic designations |
uemoa-gel |
UEMOA / BCEAO asset-freezing lists |
import:<name> |
Any other list for every workspace. <name> is 2 to 57 characters: a-z and 0-9, and after the first character also -, _ and .. It is created by the first import; --name gives it a display name |
- The file is the whole list. Entries missing from it are treated as removed, and open potential matches on them are marked delisted in every workspace. Always import the complete list.
unchanged (version N)means the file has the same entries as the current version, and nothing was stored.- A fetched list is refused: … is fetched from its publisher; import a signed list bundle instead.
- An import is not compared by date with the current version. It is your explicit decision, and it becomes the new version.
- The command also works on the hosted service, for SpeculaPro operations.
- It is recorded in the audit log as
screening.list.versionby the system, with no workspace.
Workspaces in Ghana, Nigeria and the UEMOA states screen against their national list by default (see Default lists by jurisdiction), but the list matches nothing until you have imported it. Check Settings → Screening in such a workspace: the list must show a version, not Not loaded yet.
Installation-wide providers
Section titled “Installation-wide providers”A screening provider can be set up once for every workspace, when one licence covers the installation. Every workspace that has no configuration of its own for that provider then uses yours. Self-hosted only: on the hosted service the commands refuse with installation-wide providers are for self-hosted installations: on the hosted service each workspace configures its own.
speculapro screening provider listspeculapro screening provider set <provider> [--base-url u] [--data-url u] [--dataset d] [--group-id g] [--credentials-file f] [--sync] [--disabled]speculapro screening provider remove <provider>| Flag | Meaning |
|---|---|
<provider> |
opensanctions, complyadvantage or worldcheck. See which providers work |
--base-url |
The provider’s API address. Empty: its default |
--data-url |
OpenSanctions bulk data address, for the feed. Empty: data.opensanctions.org |
--dataset |
OpenSanctions dataset (default, sanctions, peps) |
--group-id |
World-Check One group id. Required for worldcheck |
--credentials-file |
A JSON file holding the credentials. Leave it out to keep the stored ones |
--sync |
Load the provider’s bulk and delta feed into the list provider:<provider> every six hours. OpenSanctions only; for another provider it is refused with … has no bulk data feed to sync |
--disabled |
Keep the configuration but don’t use it |
How to set up an installation-wide provider
Section titled “How to set up an installation-wide provider”-
Write the credentials to a file readable only by you. Credentials are never given on the command line, where they would end up in the shell history and the process list:
{"api_key": "<your key>"}The file may hold
api_key,api_secret,usernameandpassword: the ones your provider uses. Any other field is refused. -
Make sure
SP_SECURITY_MASTER_KEYS(andSP_SECURITY_BLIND_INDEX_KEY) are set for the command, as for the server: the credentials are encrypted with the installation’s encryption keys before they are stored. -
Run:
Terminal window docker compose run --rm -v "$PWD/opensanctions.json:/tmp/creds.json:ro" cli \screening provider set opensanctions --dataset default --credentials-file /tmp/creds.json --syncopensanctions is configured for every workspace of this installation (enabled=true, feed sync=true) -
Delete the credentials file.
-
Check with
speculapro screening provider list, and in a workspace’s Screening providers panel, where it shows for the whole installation.
Things to know:
- Each
setstates the whole configuration. A flag you leave out goes back to its default: without--syncthe feed is switched off, without--disabledthe provider is on, without--base-urlthe default address is used. Only the credentials are kept when you leave their flag out. - Addresses pass the same checks as a workspace’s:
https, no user name or password, not an internal host unlessSP_SCREENING_ALLOW_PRIVATE=true. - The feed. With
--sync, the worker loads the provider’s data into the installation listprovider:opensanctions: the whole dataset the first time, then the changes, every six hours. Workspaces tick that list under Settings → Screening. Provider data is licensed to your installation; check that your licence covers every workspace on it. - Removing a provider (
provider remove) stops its use and its feed. The list its feed loaded stays, at its last version; it gets no new versions. provider setandprovider removeare recorded in the audit log asscreening.provider.saveandscreening.provider.deleteby the system, withinstallation: true. The credentials themselves are never recorded, only whether they changed.
Provider failures and retries
Section titled “Provider failures and retries”A provider that can’t be reached is not silently skipped. For each customer or counterparty it could not be asked about, the worker records the subject as not fully screened with the provider’s error, leaves that provider’s earlier matches on the subject open, and tries again:
| First retry | After 1 minute |
| Later retries | The wait doubles each time, up to 6 hours |
| How long | For as long as the provider stays configured and enabled |
| Within one pass | After three failures in a row, the provider is left alone for the rest of that pass; the subjects are retried later |
| A lists-only re-screen | Does not cancel a pending retry |
| The provider is removed or disabled | The retries end |
The number of subjects waiting is the metric speculapro_screening_provider_retries_pending. A
value that stays above zero means some people are screened against the lists only. See
What to watch and
Providers: when a provider can’t be reached.
Air-gapped installations
Section titled “Air-gapped installations”- Set
SP_SCREENING_AIR_GAPPED=trueon every process (Helm:screening.airGapped: true, which also closes the screening egress rule). Nothing is fetched from then on. - Get a signed list bundle from SpeculaPro on a connected machine, on the schedule your policy requires. Lists change often; a bundle is a snapshot.
- Carry it across with your approved media.
- Verify and import it (below).
Until the first bundle is imported, every list shows Not loaded yet, nobody is matched, and
screening a name through the API answers 409.
Providers are a separate matter: a hosted provider can’t be reached from an air-gapped network. A
yente server on your own network can, with SP_SCREENING_ALLOW_PRIVATE=true.
When a list loses many entries
Section titled “When a list loses many entries”A fetched file or a signed bundle that parses correctly but has lost most of its entries would end screening against the missing entries in every workspace, and mark the open matches on them delisted. A publisher’s error or a tampered source could cause it. So SpeculaPro refuses such a version until you accept it.
| What is checked | A fetch from a publisher and a signed bundle (from the command line or the API) |
| What is not | speculapro screening import-list, a workspace’s own import, and provider feeds |
| When it is refused | The new version would remove more than SP_SCREENING_MAX_REMOVAL_PERCENT (20) per cent of the current version’s entries, and the current version has at least SP_SCREENING_MAX_REMOVAL_MIN_ENTRIES (50) entries |
| What happens | Nothing is stored and the current version stays. For a bundle, no list in the bundle is stored |
| How you see it (fetch) | The list’s status is error, shown as Failed under Settings → Screening, with the counts as the reason. The worker logs screening: fetched list refused, the current version stays at error level. The metric counts it under result="refused", not error |
| How you see it (bundle) | The command or the API answers with the same message |
The message names the list and the numbers, and the command that accepts it:
invalid: large removal refused: the new un-sc would remove 600 of the current version's 1012 entries (59%) and leave 412. The current version (5) stays. If the publisher really removed them, accept it with: speculapro screening source fetch un-sc --accept-removalHow to accept a large removal
Section titled “How to accept a large removal”-
Open the publisher’s own website and confirm that it announced the removals. Compare the entry count in the message with the publisher’s file. If the numbers don’t agree, or you can’t find an announcement, don’t accept: leave the current version in place and contact the publisher or SpeculaPro support.
-
For a fetched list, fetch it again and accept the removal for this one fetch:
Terminal window docker compose run --rm cli screening source fetch un-sc --accept-removalun-sc version 6: 412 entries (+0, ~0, -600) -
For a bundle, import it again with the same flag:
Terminal window docker compose run --rm -v "$PWD/lists-2026-10-10.splists:/tmp/lists.splists:ro" cli \screening import-bundle /tmp/lists.splists --accept-removalThe flag applies to every list in that bundle.
Things to know:
--accept-removallifts the check for that one command only. The worker’s next fetch is checked again as usual.speculapro screening source fetch <key>without the flag fetches one list now, with the check. It works for fetched lists only, and is refused on an installation withSP_SCREENING_AIR_GAPPED=true: this installation never fetches lists from their publishers (SP_SCREENING_AIR_GAPPED): import a signed list bundle instead.- A bundle sent through the API can be accepted there too: the same request with
?accept_removal=true, by someone who holdsscreening.accept_removal(by default the MLRO and the administrator) as well asscreening.manage. It is audited under their name asscreening.bundle.accept_removal, and when approval applies the approver sees ACCEPTING LARGE REMOVALS in the request. See Screening API: import a signed bundle. - The stored version is audited as
screening.list.version, with the counts removed, like any other version. - Each workspace’s worker pass then marks the open matches on the removed entries delisted, as for any list change.
- Setting
SP_SCREENING_MAX_REMOVAL_PERCENT=100switches the check off. Don’t, unless you compare every new version yourself.
Signed list bundles
Section titled “Signed list bundles”A bundle is a file (.splists) holding one or more complete lists, signed by SpeculaPro with an
Ed25519 key. It uses the same envelope as signed rule packs but
different keys: a rule-pack key can’t sign lists, and a list key can’t sign rules or licences. The
keys that check it are built into your release (key id lists-2026-1 at the time of writing).
SpeculaPro checks the signature before it reads anything inside the file. A bundle can be up to 96 MB.
A bundle can carry any built-in list, including the ones with no feed (Nigeria, Ghana, UEMOA), and
lists named import:<name>, which are created if the installation doesn’t have them. A bundle that
carries an import:<name> key a workspace uses for its own list is refused.
All the lists in a bundle are stored together or not at all. If one list can’t be stored, none is.
A fetch and a bundle for the same list
Section titled “A fetch and a bundle for the same list”A fetched list can get a version from its publisher or from a bundle. The rule is that a list never goes back in time: the newest publication wins.
| What arrives | Stored when |
|---|---|
| A list in a bundle | It is published after the installation’s current version of that list. The date compared is the list’s publication date in the bundle, or the bundle’s creation date when the list has none |
| A list in a bundle, when the current version has no publication date (a file you imported that states none) | The bundle was made after the moment that version was stored |
| A fetched file that states its publication date | That date is not before the current version’s. On the same date the publisher’s file wins |
| A fetched file with no date of its own (South Africa FIC, Canada SEMA) | Always: it is the publisher’s current answer |
| The same entries as the current version | Never: reported as unchanged |
What you see when the current version is kept:
- From the command line:
us-ofac-sdn kept at version 9: the installation's version is newer than this file. - In the API’s answer:
"older": truefor that list. - For a fetch: the list’s status is
older, shown as Newer version kept under Settings → Screening, and counted under the metric labelresult="older".
So bundles and live feeds can be used for the same list. A bundle that is ahead of the publisher’s server is kept until the publisher catches up, and an old bundle never replaces a newer fetch.
Imports with import-list are not compared: they are your explicit decision.
How to check a bundle
Section titled “How to check a bundle”speculapro screening verify-bundle lists-2026-10-10.splistsOK: signed with lists-2026-1 on 2026-10-10T06:00:00Z un-sc 1012 entries, published 2026-10-09 us-ofac-sdn 17804 entries, published 2026-10-09(The counts are illustrative.) verify-bundle changes nothing and needs no database. A bundle that
doesn’t verify stops with list bundle is not valid: ….
How to import a bundle from the command line
Section titled “How to import a bundle from the command line”# Docker Composedocker compose run --rm -v "$PWD/lists-2026-10-10.splists:/tmp/lists.splists:ro" cli \ screening import-bundle /tmp/lists.splistsun-sc version 5: 1012 entries (+2, ~1, -0)us-ofac-sdn unchanged (version 9)eu-fsf kept at version 14: the installation's version is newer than this file- The command connects with
SP_DB_SYSTEM_URLwhen it is set, otherwiseSP_DB_URL. Run it with the system role’s connection (or the schema owner’s): the application role is not allowed to write the installation’s lists. - The file must be readable by the container’s user (UID 65532), for example
chmod 644. - There is no size limit beyond the 96 MiB a bundle may be.
- A bundle whose list would remove a large part of the current version is refused, with nothing
stored, unless you add
--accept-removal. See When a list loses many entries. - The import is recorded in the audit log as
screening.list.versionby the system, with no workspace, so it doesn’t appear in any workspace’s audit page. - The command also works on the hosted service, where SpeculaPro operations use it. Only the API route is self-hosted-only.
Each workspace’s worker pass then applies the new versions, as for a fetched list.
Through the API
Section titled “Through the API”POST /api/v1/screening/bundles, by someone with screening.manage, on a self-hosted
installation only.
It goes through approval, under the action type Screening sources and thresholds
(screening.config) of the workspace’s approval policy:
- The bundle’s signature is checked first. A bundle that doesn’t verify is refused, and no request is made.
- The file is kept on the server, in the table
screening_bundle_uploads. The change request carries the file’s SHA-256, the id of the key that signed it and the lists it contains, not the file. - A second person with
screening.approveapproves it under Approvals as usual. - On approval the kept file is checked against the SHA-256, verified again, imported, and removed.
- A kept file that nobody approved is removed by the worker after 8 days.
- If a list in the bundle got a newer version since the proposal, the request is closed as out of date (stale) and the bundle must be sent again.
- With the approval policy off for screening, the bundle is imported at once.
- A bundle whose list would remove a large part of the current version is refused when it is
imported (on approval, when approval applies), unless the request carries
?accept_removal=truefrom someone withscreening.accept_removal. See When a list loses many entries. - The request is limited by
SP_INGEST_MAX_BODY_BYTES(8 MiB by default); the bundle format itself allows 96 MiB. For a bundle of the large lists, raise that setting or use the command line.
Because the bundle changes the lists of every workspace on the installation, the request’s summary says so: Import a signed list bundle for every workspace of this installation: …. See Screening API.
After an upgrade that changes matching
Section titled “After an upgrade that changes matching”A release that changes how names are scored doesn’t go back over potential matches already raised,
and no migration queues a re-screen. After upgrading to the release that stopped generic words
(Wallet, Agent, Bank) and loosely spelled single words from matching, or that treats a three-letter name one letter short of another (Ama, Amar) as a different name, each workspace needs
one full re-screen to close the old false matches. There is no command for it: someone with
screening.manage in each workspace selects Re-screen everyone under Settings → Screening,
or calls POST /api/v1/screening/rescreen. Open matches that no longer reach the threshold close as
No longer matches; decided matches are untouched. Full re-screens run one after another in the
worker, lists only, without calling providers. See
How matching works.
What to watch
Section titled “What to watch”Metrics
Section titled “Metrics”worker and api processes report these on the metrics listener:
| Metric | Type | Labels | What it shows |
|---|---|---|---|
speculapro_screening_list_fetches_total |
Counter | source (the list key), result (ok, unchanged, older, error, refused) |
List fetches. ok: a new version was stored; unchanged: the publisher’s file was the one already held; older: the publisher’s file was older than the version held, which stays; error: the fetch failed; refused: the file would have removed too much of the list, and the current version stays. Every fetched list has all five series from start-up, at zero |
speculapro_screening_list_last_refresh_timestamp_seconds |
Gauge | source |
Fetched lists only. When the list was last confirmed current, as a Unix time: a successful fetch (changed or not), or a newer version stored from a signed bundle. Only lists that have a version; sampled by the worker at each pass. time() - this is the list’s age. Imported lists are not exported: they are as old as their last import, by design |
speculapro_screening_list_entries |
Gauge | source |
Entries in the current version of each installation list that has one |
speculapro_screening_subjects_screened_total |
Counter | type (customer, counterparty) |
Customers and counterparties screened |
speculapro_screening_hits_total |
Counter | kind (list, provider) |
New potential matches recorded |
speculapro_screening_provider_requests_total |
Counter | provider, result (ok, error) |
Calls to screening providers |
speculapro_screening_provider_retries_pending |
Gauge | Subjects not fully screened: a provider call failed and waits for its retry. Every workspace together |
source is always one of the installation’s lists. A workspace’s own lists are never a label,
so the number of series stays small and no workspace’s list names reach your monitoring.
decision processes report these for inline screening:
| Metric | Type | Labels | What it shows |
|---|---|---|---|
speculapro_decision_inline_screenings_total |
Counter | result (clear, match, sanctions_match) |
Counterparties a decision screened itself because nothing was stored for them. match is a potential match on a list that is not a sanctions list; sanctions_match is one the decision’s sanctions policy acts on |
speculapro_decision_inline_screening_duration_seconds |
Histogram | Time a decision spent screening a counterparty’s name against the in-memory index | |
speculapro_decision_inline_records_total |
Counter | result (recorded, none, error, dropped) |
Potential matches handed on for review. none: nothing to record, because a reviewer already decided the match or the stored counterparty doesn’t match. error: the write failed. dropped: the queue was full. After error or dropped the worker records the match when it screens the counterparty |
A rising error or dropped count loses nothing for review, but until the worker’s next pass
later payments to that counterparty are screened inline again rather than reading a stored match.
Alerts
Section titled “Alerts”The Helm chart’s PrometheusRule carries four alerts, when screening.enabled. On other platforms,
build the same from the expressions:
| Alert | Fires when | What to do |
|---|---|---|
SpeculaProScreeningListStale |
time() - max by (source) (speculapro_screening_list_last_refresh_timestamp_seconds) above 48 hours (prometheusRule.thresholds.screeningListStaleHours), for 30 minutes |
People are screened against an old copy of that list. Check the worker and its outbound access; on an air-gapped installation, import a newer bundle |
SpeculaProScreeningListFetchFailing |
A list’s fetches failed and none succeeded (ok, unchanged or older) in the last 6 hours, for 2 hours |
Read the error on Settings → Screening or in the worker log. A 404 usually means the publisher moved the list |
SpeculaProScreeningListRefused |
A new version of a list was refused in the last 6 hours because it would remove too many entries (critical; fires on the first refusal) | Compare the file with the publisher’s notices, then accept it or leave it. The current version stays in use meanwhile |
SpeculaProScreeningProviderErrors |
More than 20% of calls to a provider failed over 15 minutes (prometheusRule.thresholds.screeningProviderErrorRatio), for 15 minutes |
Subjects are still screened against the lists, but this provider’s results are missing, and the failed subjects wait for a retry (speculapro_screening_provider_retries_pending). Check the provider’s credentials and status, and outbound access from api and worker |
The stale-list alert concerns fetched lists only. A list you import with import-list has no
series and never fires it: it is as old as your last import, by design.
On an air-gapped installation nothing is fetched, so a fetched list is refreshed only when you
load a bundle with a newer version of it, and it ages from that day. Set
prometheusRule.thresholds.screeningListStaleHours to your bundle cadence (for example 192 for a
weekly bundle), or the alert fires 48 hours after every bundle.
The worker also logs what it does (component=screening):
| Log line | Meaning |
|---|---|
screening: list version stored |
A list changed: source, seq, entries, added, changed, removed |
screening: list fetch failed |
A fetch failed: source and the error. It is retried after a quarter of the list’s interval |
screening: list index built |
The index was rebuilt: entries, sources, took |
screening: delta re-screen |
A list change was applied to a workspace: source, versions, subjects, hits, took |
screening: full re-screen |
A full re-screen finished: subjects, hits, took, err |
screening: new and changed subjects screened |
Everyday screening: subjects, hits |
screening: provider search failed; the subject will be retried |
A provider call failed for one subject: tenant, provider and the error. The subject is recorded as not fully screened and is retried with a growing wait. See Provider failures and retries |
screening: provider retries |
Subjects whose provider call failed earlier were screened again: subjects, hits |
screening: provider sync failed |
An installation-wide provider’s data feed could not be loaded: provider and the error. It is tried again at the next six-hourly sync |
screening: hits on lists no longer screened closed |
A workspace’s open potential matches on lists it no longer screens against were closed: tenant, hits |
screening: uploaded bundles not cleaned up (warning) |
Bundles left over from unapproved requests could not be removed; the next pass tries again |
screening: workspace (error) |
A workspace’s pass failed; the next pass tries again |
The same fetch status is on Settings → Screening for every workspace: Updated, No change, Newer version kept or Failed, with the time.
Without Prometheus, things worth an alert in your log system:
list fetch failedfor the samesourcefor more than a day;- no
list version storedfor a list that normally changes weekly (the UN and OFAC lists do); provider search failed; the subject will be retried, repeating for the sameprovider.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | What to do |
|---|---|---|
| Every list shows Not fetched yet or Not loaded yet | No worker is running, SP_SCREENING_ENABLED=false, or SP_SCREENING_AIR_GAPPED=true with no bundle imported |
Start a worker; check the settings; import a bundle |
| A list shows Failed | The publisher is down, moved the file, or the worker has no route to it. Hover over the status for the error | Check outbound access from the worker. the publisher answered 404 on Kenya usually means a new list was published at a new address: set the new address |
A fetch or provider call fails with destination not allowed |
The address resolved to a private or reserved network address | A split-horizon DNS or proxy is in the way; give the worker a direct route. For a list mirror or yente server on your own network, set SP_SCREENING_ALLOW_PRIVATE=true |
| Re-screens stay Waiting | No worker, or the worker’s passes are failing | Check the worker log for screening: errors |
409 no list has been loaded yet from the API |
No list the workspace screens against has a version | Wait for the first fetch, or import |
| A provider shows an error in the panel | Wrong credentials (the provider refused the credentials), or unreachable (the provider is unavailable) |
Fix the configuration (through the API for a workspace’s provider, speculapro screening provider set for an installation-wide one); check outbound access from api and worker |
speculapro_screening_provider_retries_pending stays above zero |
A provider keeps failing, so subjects wait for their retry and are screened against the lists only | Fix the provider, or disable or remove it if it is no longer used; the retries then end |
| A list shows Failed with large removal refused | The publisher’s file would remove more than 20% of the list | Check the publisher’s announcement, then accept it or leave it. Nothing is lost meanwhile: the current version stays in use |
| A list shows Newer version kept | The publisher’s file is older than the version loaded from a bundle | Nothing. The newer version stays until the publisher catches up |
| A workspace in Ghana, Nigeria or a UEMOA state shows its national list as Not loaded yet | The list is ticked by default but you have not imported it | Import it |
A workspace’s import of ng-nigsac, gh-domestic or uemoa-gel through the API answers 403 |
Those are the installation’s lists | Import the file yourself with speculapro screening import-list, or the workspace imports its own import:<name> list |
| The queue is full of matches on words like Wallet or Agent after an upgrade | They were raised before generic words stopped matching | Have each workspace re-screen everyone once |
A process doesn’t start, with config: SP_SCREENING_… |
A screening setting is out of range or an address override is invalid | See What stops start-up |
import-list or a bundle answers … is the key of a workspace’s own list |
A workspace already has a list of its own under that import:<name> key |
Use another key for the installation’s list |