Developer guide

UK sanctions API in Python: screening and error handling

Build UK sanctions screening in Python with a complete HTTP example, explicit source selection, idempotency, safe failures, and review handoffs.

On this page

What this Python integration does

The UK Sanctions List is the current government designation source. The former OFSI Consolidated List ceased updating on January 28, 2026. This tutorial uses SanctionsKit’s documented UK source identifier, not an invented government matching API.

The client submits a subject, validates the response, and prints only the screening ID, status, and candidate count. It does not approve a customer, resolve an identity, or make a legal decision. A nonzero exit leaves the local operation incomplete.

Prepare the runtime and a synthetic request

Use Python 3.11 or later. The HTTP client uses urllib, json, and the standard library, so the screening example needs no pip package or provider SDK.

Save the following as request.json. Mira Calder is an invented subject and sandbox@1 uses synthetic fixtures, not a current production sanctions search. Set SANCTIONSKIT_API_KEY to your test key in the process environment. Set SCREENING_OPERATION_ID to a new persistent identifier for this operation, and reuse it only for an unchanged retry.

{
  "subject": {
    "name": "Mira Calder",
    "entityType": "person",
    "birthDate": "1984"
  },
  "package": "sandbox@1",
  "retention": "standard"
}

Send the request and validate the result

Save the client as screen.py. The endpoint is fixed to SanctionsKit’s HTTPS API. The example rejects redirects, requires explicit authorization before using a live key, and does not retry automatically. Production integrations should preserve the operation state before the first network call.

"""Python 3.11+. Trusted backend/CLI. Uses only the standard library."""
import json
import os
import sys
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, build_opener, HTTPRedirectHandler

ENDPOINT = "https://www.sanctionskit.com/api/v1/screenings"
MAX_BYTES = 8 * 1024 * 1024

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

def main() -> None:
    key = os.environ.get("SANCTIONSKIT_API_KEY", "")
    operation = os.environ.get("SCREENING_OPERATION_ID", "")
    if not key or not operation:
        raise ValueError("Set the API key and a persistent operation ID.")
    if not key.startswith("sk_test_") and not (
        key.startswith("sk_live_") and os.environ.get("ALLOW_LIVE_SCREENING") == "yes"
    ):
        raise ValueError("Use a test key, or explicitly authorize a live screening.")
    raw = Path(sys.argv[1] if len(sys.argv) > 1 else "../request.sandbox.json").read_bytes()
    if len(raw) > 64 * 1024:
        raise ValueError("Request file exceeds this example's 64 KiB limit.")
    body = json.loads(raw)
    if not isinstance(body, dict) or not body.get("subject") or ("sources" in body) == ("package" in body):
        raise ValueError("Supply a subject and exactly one of sources or package.")
    request = Request(ENDPOINT, data=json.dumps(body).encode(), method="POST", headers={
        "Authorization": f"Bearer {key}", "Content-Type": "application/json", "Idempotency-Key": operation,
    })
    try:
        with build_opener(NoRedirect()).open(request, timeout=25) as response:
            status, payload = response.status, response.read(MAX_BYTES + 1)
    except HTTPError as error:
        with error:
            status, payload = error.code, error.read(MAX_BYTES + 1)
    if len(payload) > MAX_BYTES:
        raise ValueError("Incomplete response: size limit exceeded.")
    envelope = json.loads(payload)
    if not 200 <= status < 300:
        detail = envelope.get("error", {}) if isinstance(envelope, dict) else {}
        code = detail.get("code", "unknown") if isinstance(detail, dict) else "unknown"
        raise ValueError(f"Screening incomplete: HTTP {status}, code {code}.")
    data = envelope.get("data") if isinstance(envelope, dict) else None
    if not isinstance(data, dict) or not isinstance(data.get("id"), str) or not data["id"]:
        raise ValueError("Incomplete response: missing screening ID.")
    state, matches = data.get("status"), data.get("matches")
    if state not in ("potential_match", "no_match") or not isinstance(matches, list):
        raise ValueError("Incomplete response: unexpected screening shape.")
    if (state == "no_match" and matches) or (state == "potential_match" and not matches):
        raise ValueError("Incomplete response: inconsistent screening shape.")
    print(json.dumps({"id": data["id"], "status": state, "candidateCount": len(matches)}))

if __name__ == "__main__":
    try:
        main()
    except (OSError, ValueError, URLError) as error:
        # Do not print the request, credential, or raw API response.
        print(f"Screening did not complete: {type(error).__name__}. Check the operation before retrying.", file=sys.stderr)
        sys.exit(1)

Run the client

Run this from the directory containing request.json and the project files. Keep the API key out of committed files and shell tracing. The tutorial’s request-file limit is 64 KiB and its response parsing limit is 8 MiB; these are client safeguards, not claims about API service limits.

urllib raises HTTPError for unsuccessful HTTP responses, so handle that branch separately from network errors. Disable redirects and bound the body read. Validate object, string, and list types explicitly; an exception must not return an empty matches array.

python screen.py request.json

Change to the required production coverage

Select uk-sanctions only after checking the current source metadata. Preserve the authority’s designation reference and the applicable measures in downstream review. A legacy OFSI feed name is not evidence that a dataset is current.

For an authorized production operation, replace the package field with the sources array shown below, supply the actual lawfully held subject details, use a live key, and set ALLOW_LIVE_SCREENING=yes. Keep the complete request in request.json and create a new operation ID. Do not send both sources and package. The fragment below is a coverage change, not a complete request.

{
  "sources": [
    "uk-sanctions"
  ]
}

Preserve the source details a reviewer needs

Keep individual name components, original-script names where provided, aliases, dates, identifiers, and designation references. Do not flatten every UK designation into the same financial restriction or reuse a U.S. ownership test as a UK ownership-and-control conclusion.

Retain the screening ID with your internal subject reference. Retrieve the complete evidence through the documented retained-result workflow when authorized; the console summary deliberately does not print raw identity details. A later analyst decision must not overwrite the original screening outcome.

Handle failures and test the source-specific edge cases

Treat HTTP errors, timeouts, malformed JSON, missing fields, unknown statuses, and unavailable required coverage as incomplete checks. A 401 or 403 needs credential or scope correction. A 409 needs idempotency-conflict handling. A 429 needs its specific rate-limit or usage-cap treatment; it is not a no-match result.

Include a migration test proving an old OFSI snapshot cannot silently replace the current approved UK dataset. Test original-script names, aliases, changed designation evidence, and missing identifying fields.

Use unittest or your existing test runner to replace the opener with synthetic responses. Exercise HTTPError, malformed JSON, a missing data object, and an unknown status as well as the two valid result paths.

  • Verify both valid result states and inconsistent status/matches combinations.
  • Retry an accepted operation only with its original key and unchanged payload.
  • Keep missing identity facts unknown instead of inventing contradictory values.
  • Route potential matches into review before the separate business decision.
  • Keep secrets, submitted names, and raw error bodies out of routine logs.

Official references