PlatformXeDocs
Get API Key

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

PropertyValue
Scopefraud:screen
Plan gateDetection Pack addon (Pro or Enterprise)
Rate limit1,000 requests / hour per API key
IdempotentYes (Idempotency-Key header honoured)

Request body

FieldTypeRequiredDescription
namestringYesThe name to screen (≤ 200 chars). Will be normalised before matching.
dobstringNoISO-8601 date. Used as a soft filter when present on the matched entry.
countrystringNoISO 3166-1 alpha-2 country code. Narrows the search to entries with the same or no country.
identifiersRecord<string,string>NoTenant/provider-specific ids (BVN, NIN, passport). Stored verbatim; not used for fuzzy match.
kindsArray<ScreeningListKind>NoRestrict to specific list kinds. Default: ['sanctions', 'pep', 'tenant_blocklist'].
thresholdnumber (0.3–1)NoTrigram similarity threshold. Default 0.6. Below 0.3 is clamped — pg_trgm's GIN index has no signal there.
decisionIdstringNoBind 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" }
      }
    ]
  }
}
FieldTypeDescription
screenIdstringUnique 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.
fallbackbooleantrue 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.
screenedAtstringISO-8601 timestamp of the screen.
hitsScreeningHit[]Up to 25 hits, ordered by descending similarity. Empty when nothing matches above the threshold.

Hit shape

FieldTypeDescription
entryIdstringStable id of the matched entry.
listSourcestringSource label, e.g. OFAC_SDN, tenant:org_acme:internal-blocklist.
listKindScreeningListKindKind of the list the hit came from.
scorenumber (0..1)Trigram similarity. Higher is closer.
fieldsMatchedstring[]Subset of fields that contributed (always includes name).
reasonstringHuman-readable, safe to surface to ops. No PII beyond what was queried.
matchedRecord<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_lists rows 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.

SourceKindFormatRefresh URL (override env var)Default frequency
OFAC_SDNsanctionsXMLOFAC_SDN_URLDaily
UN_CONSOLIDATEDsanctionsXMLUN_CONSOLIDATED_URLDaily
EU_CONSOLIDATEDsanctionsXMLEU_CONSOLIDATED_URLDaily
UK_HMTsanctionsCSVUK_HMT_URLDaily
NFIU_PEPpepJSON or CSVNFIU_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 raw for 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:

  1. Call /api/v1/fraud/screen and inspect the hits.
  2. 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.
  3. Pass the screenId into your next /decide call's context so 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

AspectValue
Hot path latency (free lists, p95)< 80ms
Hot path latency (commercial, p95)250–800ms (vendor-dependent)
Result retention90 days (Pro) / 7 years (Enterprise)
Hits per responseUp 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

HTTPCodeCause
400BAD_REQUESTMissing or oversized name, invalid kinds or threshold.
401UNAUTHORIZEDMissing or invalid API key.
402DETECTION_PACK_REQUIREDDetection Pack addon not enabled.
403FORBIDDENAPI key has no organisation context, or wrong scope.
429RATE_LIMITED1,000/hr ceiling exceeded.
500INTERNAL_ERROREvery 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({ /* ... */ });
}