Screening
Sanctions, PEP, adverse-media, and tenant-list screening with multi-provider fallback and full audit trail.
The Fraud Detection Engine ships a synchronous screening endpoint that matches a query against:
- Free public lists — OFAC SDN, UN consolidated, EU consolidated, UK HMT, NFIU public PEP.
- Tenant-managed allowlists and blocklists.
- Commercial sanctions / PEP / adverse-media coverage when configured.
The engine composes a fallback chain so a single provider outage does not silently degrade your verdict trail.
Endpoint
POST /api/v1/fraud/screen
| Property | Value |
|---|---|
| Scope | fraud:screen |
| Plan gate | Detection Pack addon (Pro or Enterprise) |
| Rate limit | 1,000 requests / hour per API key |
| Idempotent | Yes (Idempotency-Key header honoured) |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The name to screen (≤ 200 chars). Will be normalised before matching. |
dob | string | No | ISO-8601 date. Used as a soft filter when present on the matched entry. |
country | string | No | ISO 3166-1 alpha-2 country code. Narrows the search to entries with the same or no country. |
identifiers | Record<string,string> | No | Tenant/provider-specific ids (BVN, NIN, passport). Stored verbatim; not used for fuzzy match. |
kinds | Array<ScreeningListKind> | No | Restrict to specific list kinds. Default: ['sanctions', 'pep', 'tenant_blocklist']. |
threshold | number (0.3–1) | No | Trigram similarity threshold. Default 0.6. Below 0.3 is clamped — pg_trgm's GIN index has no signal there. |
decisionId | string | No | Bind the screen result to an existing fraud_decisions row so audits surface the link. |
Response
{
"success": true,
"data": {
"screenId": "scr_2j7yhq8wcs9...",
"provider": "free_lists",
"fallback": false,
"screenedAt": "2026-05-03T09:14:21.412Z",
"hits": [
{
"entryId": "slse_abc123",
"listSource": "OFAC_SDN",
"listKind": "sanctions",
"score": 0.83,
"fieldsMatched": ["name", "country"],
"reason": "Fuzzy name match: \"jane doe\" ↔ \"jane doe smith\" (83% similar)",
"matched": { "name": "Jane Doe Smith", "country": "NG" }
}
]
}
}
| Field | Type | Description |
|---|---|---|
screenId | string | Unique id for this screen — surface to your reviewers. Persisted in fraud_screen_results. |
provider | "commercial_primary" | "free_lists" | "cached_last_known" | "tenant_only" | Which provider in the fallback chain produced the hits. |
fallback | boolean | true when the commercial provider was unavailable and the engine fell back to free public lists. Treat fallback responses as advisory if you rely on commercial coverage. |
screenedAt | string | ISO-8601 timestamp of the screen. |
hits | ScreeningHit[] | Up to 25 hits, ordered by descending similarity. Empty when nothing matches above the threshold. |
Hit shape
| Field | Type | Description |
|---|---|---|
entryId | string | Stable id of the matched entry. |
listSource | string | Source label, e.g. OFAC_SDN, tenant:org_acme:internal-blocklist. |
listKind | ScreeningListKind | Kind of the list the hit came from. |
score | number (0..1) | Trigram similarity. Higher is closer. |
fieldsMatched | string[] | Subset of fields that contributed (always includes name). |
reason | string | Human-readable, safe to surface to ops. No PII beyond what was queried. |
matched | Record<string,unknown> | Sanitised payload from the list. |
Multi-provider fallback chain
commercial primary → free public lists → cached last-known
- Commercial primary — covers sanctions / PEP / adverse-media via a contracted vendor. Returns the highest-quality hits when available. (Procurement-dependent: see the overview for current status.)
- Free public lists — pg_trgm fuzzy match against
screening_listsrows ingested by the daily refresh cron plus tenant blocklists. The cron pulls the canonical feeds in their native formats and normalises them into the same row shape, so the engine's fuzzy match works uniformly across sources. See Public list sources below for the full set. - Cached last-known — short-lived backstop when both upstreams are unavailable.
When the commercial provider is unavailable, the engine emits FRAUD_PROVIDER_DOWN and the response carries fallback: true. Subscribe via webhooks to alert your SOC.
Public list sources
The daily refresh cron (/api/cron/screening-list-refresh) pulls each source in its native format, parses it into the canonical row shape, and replaces the existing entries for that source. Each source identifier surfaces unchanged in the listSource field on hits.
| Source | Kind | Format | Refresh URL (override env var) | Default frequency |
|---|---|---|---|---|
OFAC_SDN | sanctions | XML | OFAC_SDN_URL | Daily |
UN_CONSOLIDATED | sanctions | XML | UN_CONSOLIDATED_URL | Daily |
EU_CONSOLIDATED | sanctions | XML | EU_CONSOLIDATED_URL | Daily |
UK_HMT | sanctions | CSV | UK_HMT_URL | Daily |
NFIU_PEP | pep | JSON or CSV | NFIU_PEP_URL (mirror — disabled until set) | Daily when configured |
Each source supports an optional override URL via the env vars above. Sources with no configured URL (currently NFIU until a mirror is contracted) are silently skipped — LIST_SOURCES.length === 0 is no longer the production state.
Alias handling
Sanctions lists publish the canonical name plus aliases ("AKAs"). The ingester emits one row per alias linked to the primary via identifiers.aliasOf — so a hit on an alias still traces back to the parent entity in raw.akaOf / raw.primaryName.
What we DON'T do
- We do NOT crack open the canonical XSDs / DTDs of each source. The parsers are tuned to the published element-set we care about (name + DOB + nationality + identifiers); other metadata travels in
rawfor forensic purposes only. - We do NOT issue a hit-by-hit explanation back to the publisher. If a source removes an entry, the next refresh removes it from our index too — no soft-deletes, no historical retention beyond the 90-day audit window on
fraud_screen_results.
Verdict semantics
A hit by itself is a signal, not a decision. To turn it into a verdict:
- Call
/api/v1/fraud/screenand inspect the hits. - If the top hit's score is above your tenant-defined threshold (typically 0.75 for sanctions, 0.7 for PEP), open a fraud case and stop the action.
- Pass the
screenIdinto your next/decidecall'scontextso the rule engine can incorporate the signal.
The engine does NOT auto-block on a screen hit — verdicts come from rules + scoring. This keeps screening declarative and lets tenants tune severity per list kind.
Performance + retention
| Aspect | Value |
|---|---|
| Hot path latency (free lists, p95) | < 80ms |
| Hot path latency (commercial, p95) | 250–800ms (vendor-dependent) |
| Result retention | 90 days (Pro) / 7 years (Enterprise) |
| Hits per response | Up to 25, ordered by score desc |
Every screen is recorded in fraud_screen_results with the sanitised query, the hit set, and the provider that returned them — full audit reproducibility regardless of which leg of the fallback chain answered.
Error responses
| HTTP | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST | Missing or oversized name, invalid kinds or threshold. |
| 401 | UNAUTHORIZED | Missing or invalid API key. |
| 402 | DETECTION_PACK_REQUIRED | Detection Pack addon not enabled. |
| 403 | FORBIDDEN | API key has no organisation context, or wrong scope. |
| 429 | RATE_LIMITED | 1,000/hr ceiling exceeded. |
| 500 | INTERNAL_ERROR | Every provider in the chain failed (see the BetterStack status page). |
Examples
curl
curl -X POST https://api.platformxe.com/api/v1/fraud/screen \
-H "Content-Type: application/json" \
-H "x-api-key: pxk_live_your_api_key_here" \
-d '{
"name": "Jane Doe",
"country": "NG",
"kinds": ["sanctions", "pep", "tenant_blocklist"],
"threshold": 0.65
}'
TypeScript SDK
import { PlatformXe } from '@caldera/platformxe-sdk';
const px = new PlatformXe({ apiKey: process.env.PLATFORMX_API_KEY! });
const result = await px.fraud.screen({
name: 'Jane Doe',
country: 'NG',
kinds: ['sanctions', 'pep', 'tenant_blocklist'],
threshold: 0.65,
});
const topHit = result.data.hits[0];
if (topHit && topHit.score >= 0.75) {
// Sanctions hit at high confidence — escalate
await px.fraud.cases.open({ /* ... */ });
}