Aller au contenu

Screening lists for operators

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

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:

  1. A worker is running. Without one, no list is fetched and nobody is screened.
  2. The worker can reach the list publishers over HTTPS, or the installation is set to air-gapped and you load lists from signed bundles.
  3. Processes that run the api, worker or decision role have memory for the list index.
  4. Someone watches for fetch failures and provider errors.
  5. 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.
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.

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 persist role 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.

Every SP_SCREENING_INTERVAL (30 seconds by default), and straight away when customers are ingested by the same process, the worker:

  1. fetches each feed whose time has come (at most 20 per pass);
  2. every six hours, loads the data feed of each installation-wide provider that has one switched on;
  3. rebuilds the list index if a version changed;
  4. puts back in the queue any re-screen that has been running for more than two hours;
  5. removes signed bundles that were sent through the API and have waited more than 8 days for an approval that never came;
  6. 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;
  7. 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.

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.

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
  • to replaces the default destinations. For a yente server or list mirror on your own network, set screening.allowPrivate: true and name it under to; to keep the publishers as well, list the public block next to it:

    screening:
    allowPrivate: true
    networkPolicy:
    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, leave ports at [443] and give the other server its own rule under networkPolicy.egress.extra.

  • Network policies add up. While networkPolicy.egress.webhookPorts is [443], worker pods can already reach any address on 443, private ones included; the same goes for api pods and networkPolicy.egress.sso. Narrow or empty those if screening must be the only way out.

  • The chart refuses to render screening.allowPrivate: true with deployment: saas, and a SP_SCREENING_* setting under config (use the screening values, which the rule follows).

See Kubernetes with Helm.

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.

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.

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).

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

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-separated key=url pairs:

    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.xlsx
    ke-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-dfat and ke-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 (http only with SP_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_URLS stops start-up; see What stops start-up. An invalid address given to set-url is refused and nothing is stored.
  • With both set for the same list, set-url reminds you: note: a URL for the same list in SP_SCREENING_SOURCE_URLS takes precedence.
  • set-url and reset-url make the list due at once, and are recorded in the audit log as screening.source.url, by the system.

To go back to the built-in address:

Terminal window
docker compose run --rm cli screening source reset-url ke-frc
ke-frc is fetched from its built-in URL again

To see every installation list with its origin, current version, last fetch status and the address in use:

Terminal window
docker compose run --rm cli screening source list

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-gel or any other installation key is refused with 403, 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.

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:

Terminal window
docker compose run --rm -v "$PWD/nigsac-2026-06-18.csv:/tmp/nigsac.csv:ro" cli \
screening import-list ng-nigsac /tmp/nigsac.csv
ng-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.version by 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.

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 list
speculapro 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”
  1. 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, username and password: the ones your provider uses. Any other field is refused.

  2. Make sure SP_SECURITY_MASTER_KEYS (and SP_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.

  3. 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 --sync
    opensanctions is configured for every workspace of this installation (enabled=true, feed sync=true)
  4. Delete the credentials file.

  5. 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 set states the whole configuration. A flag you leave out goes back to its default: without --sync the feed is switched off, without --disabled the provider is on, without --base-url the 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 unless SP_SCREENING_ALLOW_PRIVATE=true.
  • The feed. With --sync, the worker loads the provider’s data into the installation list provider: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 set and provider remove are recorded in the audit log as screening.provider.save and screening.provider.delete by the system, with installation: true. The credentials themselves are never recorded, only whether they changed.

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.

  1. Set SP_SCREENING_AIR_GAPPED=true on every process (Helm: screening.airGapped: true, which also closes the screening egress rule). Nothing is fetched from then on.
  2. 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.
  3. Carry it across with your approved media.
  4. 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.

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-removal
  1. 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.

  2. 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-removal
    un-sc version 6: 412 entries (+0, ~0, -600)
  3. 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-removal

    The flag applies to every list in that bundle.

Things to know:

  • --accept-removal lifts 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 with SP_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 holds screening.accept_removal (by default the MLRO and the administrator) as well as screening.manage. It is audited under their name as screening.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=100 switches the check off. Don’t, unless you compare every new version yourself.

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 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": true for that list.
  • For a fetch: the list’s status is older, shown as Newer version kept under Settings → Screening, and counted under the metric label result="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.

Terminal window
speculapro screening verify-bundle lists-2026-10-10.splists
OK: 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”
Terminal window
# Docker Compose
docker compose run --rm -v "$PWD/lists-2026-10-10.splists:/tmp/lists.splists:ro" cli \
screening import-bundle /tmp/lists.splists
un-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_URL when it is set, otherwise SP_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.version by 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.

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:

  1. The bundle’s signature is checked first. A bundle that doesn’t verify is refused, and no request is made.
  2. 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.
  3. A second person with screening.approve approves it under Approvals as usual.
  4. 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=true from someone with screening.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.

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.

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.

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 failed for the same source for more than a day;
  • no list version stored for a list that normally changes weekly (the UN and OFAC lists do);
  • provider search failed; the subject will be retried, repeating for the same provider.
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