Screen API quickstart

Nexiel Screen is a standalone service exposing Nexiel's sanctions/PEP/AML screening as an HTTP API (POST /v1/screen), plus a liveness/mode check (GET /health). https://compliance.nexiel.io runs in production mode, so every request below needs the bearer token shown in Authentication below. Run your own instance in sandbox mode (the default when no SCREEN_API_MODE is set) to try these same requests with no key at all.

A real request/response round-trip

Health check

GET /health is routed directly on compliance.nexiel.io, and is also reachable under this service's own docs prefix as /screen-docs/health (the same handler, alongside /openapi.json and /docs). Use whichever is more convenient. For a themed, human-readable view of every service's real status, see nexiel.eu/status.

curl -s https://compliance.nexiel.io/health
{"status":"ok","mode":"production"}

The examples below show sandbox-mode responses (a self-run instance with no SCREEN_API_MODE set) so you can see a full request/response round-trip without provisioning production data first. Against https://compliance.nexiel.io itself, add -H "Authorization: Bearer <access_token>" to each request, per Authentication below, and expect real classifications against live sanctions/PEP data rather than these seeded sandbox examples.

Screen the seeded record Confirmed match

Exact name, date of birth, and nationality match:

curl -s -X POST https://compliance.nexiel.io/v1/screen \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4b9e6e2a-df1a-4c3a-9d2a-6a6a6f9e7e10" \
  -d '{"name": "Anatoly Sandboxovich Testperson", "dob": "1960-01-01", "nationality": "RU"}'
{
  "classification": "CONFIRMED_MATCH",
  "candidates": [
    {
      "entityId": "ec01cf51-0db7-4642-b106-64ff439ac8a6",
      "source": "sandbox",
      "listRef": "SANDBOX-QUICKSTART-1",
      "entityType": "INDIVIDUAL",
      "isPep": false,
      "pepCategory": null,
      "pepFunctionalCategory": null,
      "pepStatus": null,
      "pepJurisdiction": null,
      "linkedPeps": [],
      "matchedVia": [
        "exact",
        "fts",
        "phonetic",
        "trigram"
      ],
      "classifyResult": {
        "score": 100,
        "classification": "CONFIRMED_MATCH",
        "floored": false,
        "exactMatchShortCircuit": true,
        "explain": {
          "levenshtein": 1,
          "phonetic": 1,
          "dobNat": {
            "dob": 1,
            "nationality": 1,
            "combined": 1
          },
          "vector": null,
          "weightSet": "launch"
        },
        "engineVersion": "0.1.0-provisional"
      }
    }
  ],
  "wallet": {
    "classification": "CLEAR",
    "matches": [],
    "checkedAddresses": []
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false,
    "humanReviewRequired": false,
    "decisionId": "41",
    "engineVersion": "0.1.0-provisional",
    "gatesHash": "212b018655f47f1514c4b404a02e643ef027b5039964d5c68dcdd5ec0dff8f3b",
    "ensembleWeightsHash": "4a2723f74864b3371c860bbb93c3d6fbfde7dcc21e99e1242ddad3ff366cf69d",
    "listSnapshotIds": []
  }
}

entityId is a generated UUID and will differ on your machine. vector is null, because the AI similarity layer isn't live yet, so scoring here runs the 4 traditional layers only. weightSet names which named coefficient set (launch or target) produced this score. The exact per-layer weighting itself isn't published, so it's not a field on this response at all. The pep* fields carry AMLR Arts 42-46 political-exposure context (category, functional role, active/decayed status, domestic vs. foreign vs. international, and any linked PEP for a close-associate/family match), purely informational and never a factor in classification itself. The Idempotency-Key header is enforced server-side: resending this exact request with the same key returns this exact response again, without re-running the screen.

wallet is a second, independent check: exact-match cryptocurrency wallet-address screening, run whenever the request supplies a walletAddresses array (empty and CLEAR here since none was sent). meta.gatesHash, meta.ensembleWeightsHash, and meta.listSnapshotIdsare the frozen decision artifact: a SHA-256 of the exact gates configuration and ensemble weights in force, plus which list-ingestion run each live source was at, so a regulator can ask what logic produced this exact decision months later and get a precise, re-derivable answer rather than just proof the stored row wasn't tampered with.

meta.decisionIdis this decision's own handle for later lookup: pass it to GET /v1/screen/{decisionId} to re-fetch this same decision (including your own review status), or to POST /v1/screen/{decisionId}/review to record your own resolution of a POSSIBLE_MATCH (confirmed_match, false_positive, or escalated). Both routes are scoped strictly to the calling API key's own client_id: a decisionId that is real but belongs to a different account 404s exactly like one that does not exist.

A name unrelated to anything seeded Clear

curl -s -X POST https://compliance.nexiel.io/v1/screen \
  -H "Content-Type: application/json" \
  -d '{"name": "Zzqxw Unrelated Nobody"}'
{
  "classification": "CLEAR",
  "candidates": [],
  "wallet": {
    "classification": "CLEAR",
    "matches": [],
    "checkedAddresses": []
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false,
    "humanReviewRequired": false,
    "decisionId": "42",
    "engineVersion": "0.1.0-provisional",
    "gatesHash": "212b018655f47f1514c4b404a02e643ef027b5039964d5c68dcdd5ec0dff8f3b",
    "ensembleWeightsHash": "4a2723f74864b3371c860bbb93c3d6fbfde7dcc21e99e1242ddad3ff366cf69d",
    "listSnapshotIds": []
  }
}

A required field missing is a real 400

curl -s -X POST https://compliance.nexiel.io/v1/screen -H "Content-Type: application/json" -d '{}'
{
  "error": "bad_request",
  "message": "body must have required property 'name'"
}

classification (Clear, Possible match, Confirmed match) is a screening result, not a legal determination. A POSSIBLE_MATCH is never auto-cleared and always requires human review. See the legal disclaimerfor what Nexiel's outputs do and don't mean, and how sanctions and PEP screening works for what each layer above catches, which data sources and jurisdictions this covers today, and why POSSIBLE_MATCH always goes to a human.

Evidence packs: what gets encrypted, what gets kept

Register a public key first (see setting up evidence pack encryption), and every POST /v1/screen response for your account gains an evidencePack field: the name, DOB, nationality, and wallet addresses just checked, this decision's classification, listsChecked, and matchedVia, and the frozen decision artifact above, all encrypted to your key and delivered inline in this same response.

evidencePack, present only when a key is registered (truncated for this example)
{
  "evidencePack": "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSJ9..szFV0aXYxMjM.Y2lwaGVydGV4dC1nb2VzLWhlcmU.YXV0aC10YWc"
}

Present only when your account has an active registered key, omitted entirely otherwise (never null), so you can tell “no key registered” apart from “registered but empty” with a plain field check. Delivered exactly once, in this response only: Nexiel does not persist this ciphertext anywhere, and a replayed Idempotency-Key request does not re-deliver it, so capture it the first time or lose it.

Only your own private key, generated in your browser and never uploaded to Nexiel, can decrypt it. Decrypted, it holds the actual disclosed claim values, this decision (classification, listsChecked, matchedVia, humanReviewRequired), the frozen decision artifact, and an informational amlrSuggestedRetentionUntildate (this decision's own timestamp plus 5 years). That date is a courtesy reminder only. Nexiel neither enforces nor tracks it.

With a key registered, what Nexiel keeps afterward is a single screen_decisionsrow holding the request id, your client id, this decision's classification, the frozen-artifact hashes, and a SHA-256 hash of the delivered ciphertext. That hash proves the handoff happened. It does not reproduce the substantive data itself. Without a registered key, Nexiel has nothing to encrypt to, so no evidence pack exists and no such row is written. The screen still runs and returns a classification either way: only the evidence pack, and this hash-only record of it, depend on having a key registered.

AMLR makes you, the obliged entity, responsible for retaining your own CDD/screening evidence, not Nexiel, so treat the evidence pack as your own record from the moment you receive it. See setting up evidence pack encryption for how to generate and register a key from the dashboard.

Browsing the docs

The full OpenAPI 3.1 document is public at GET /openapi.json, with a browsable Swagger UI over the same document at GET /docs. Both are fully public: no API key, no signup, no “book a demo” gate, in every service mode, including production, at https://compliance.nexiel.io/screen-docs. This site's API reference covers the same endpoints in a browsable format.

Authentication, rate limiting, idempotency

A self-run sandbox-mode instance needs no API key at all. compliance.nexiel.io itself always runs in production mode, so POST /v1/screen there requires an OAuth 2.0 client-credentials access token, validated per-request via RFC 7662 introspection (so a revoked key is rejected immediately, even for a still-time-valid token), and a production key can only be minted for a client that has cleared the KYB gate.

Once approved, your dashboard's API Keys page gives you a client_id/client_secret pair, shown once. Exchange it for a short-lived access token the normal OAuth2 client-credentials way — this token, not the client secret itself, is what goes in the Authorization header:

curl -s -X POST https://auth.nexiel.io/realms/nexiel/protocol/openid-connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id=<client_id> \
  -d client_secret=<client_secret> \
  | jq -r .access_token
curl -s -X POST https://compliance.nexiel.io/v1/screen \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access_token>" \
  -d '{"name": "Anatoly Sandboxovich Testperson", "mode": "live"}'

Authenticated requests are also per-client rate limited (a token bucket: 30-request burst, 30 requests/minute sustained by default) and evaluated for three self-baselined anomaly signals: a request-volume spike, a "country never seen before for this client" geography check (country-level only, no ASN yet), and a shift in the mix of check kinds a client typically sends. A genuine volume spike automatically and temporarily reduces that client's own rate limit. The reduction is bounded and self-expiring, and it's logged as an audit event rather than applied as a permanent block. Geography and behavior-shift are logged for an operator to review and never throttle or block a request by themselves: an unfamiliar country or request pattern is human-review material under the same EU AI Act discipline that keeps POSSIBLE_MATCH out of automated hands, not grounds for an automated adverse action on its own. See the API reference for the exact 401/429 response shapes.

A client can also restrict which IP addresses or domains its own key is allowed to call from: an allowlist that scopes real requests to the client's own backend/server IPs, or the domain a browser-embedded widget runs on, so a volume-tier discount can't be resold to a third party calling from somewhere else entirely. Off by default; contact us to enable it. A request from outside a configured allowlist returns 403 client_access_restricted regardless of an otherwise-valid credential.