Developer guide

UK sanctions API in Python: customer portfolio checks

Build a Python worker for UK sanctions API checks. Screen a customer portfolio against the FCDO list and retain partial dates for review.

250 free live screenings. One lifetime allowance. No credit card required.
On this page

Check UK customers from Python

If your source system exports CSV, convert rows into the documented subject model before submission. Preserve Unicode names, identifier strings and date precision. Keep the internal row reference so two customers with the same name cannot exchange results during reconciliation.

For UK source screening, record the intended uk-sanctions coverage alongside the operation. A CSV import is an input-preparation task; it does not parse the government list or prove that an older OFSI download is current. The current source distinction belongs in your configuration and evidence records.

Prepare the synthetic exercise

Build a small invented CSV with two customers sharing a name but different internal row references. Convert each row to request.json without changing Unicode text or partial dates. Use separate saved sandbox operations to check that reconciliation cannot attach one customer’s result to the other.

Python setup and request files
RequirementValue or action
RuntimeUse 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.
Client fileSave the client as screen.py.
Screening endpointPOST /api/v1/screenings
SANCTIONSKIT_API_KEYSandbox key; screenings:write and results:read scopes
SCREENING_OPERATION_IDPersist one operation ID; reuse only with the identical request body
request.jsonMira Calder: invented subject; sandbox@1: synthetic records
request.json: synthetic sandbox request
{
  "subject": {
    "name": "Mira Calder",
    "entityType": "person",
    "birthDate": "1984"
  },
  "package": "sandbox@1",
  "retention": "standard"
}

Implement the Python request boundary

The Python worker reads a JSON request and uses urllib for discovery and screening. Treat CSV conversion as a separate input step, with explicit column mapping and rejected-row handling. A malformed input row must never become a blank-name request or a completed no-match entry.

Python: authenticated screening client
"""Python 3.11+. Run on your trusted backend; standard library only."""
import json, os, sys
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, build_opener, HTTPRedirectHandler

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

class ApiFailure(Exception): pass

def main():
    key, operation = os.environ['SANCTIONSKIT_API_KEY'], os.environ['SCREENING_OPERATION_ID']
    request = json.loads(Path(sys.argv[1] if len(sys.argv) > 1 else 'request.json').read_text(encoding='utf-8'))
    sandbox = request.get('package') == 'sandbox@1'
    source_ids = ['sandbox-synthetic'] if sandbox else request.get('sources')
    if not isinstance(source_ids, list) or not source_ids: raise ValueError('Select your sources.')
    opener = build_opener(NoRedirect())
    def api(path, body=None):
        headers = {'Authorization': 'Bearer ' + key}
        if body is not None:
            headers.update({'Content-Type': 'application/json', 'Idempotency-Key': operation})
        call = Request('https://www.sanctionskit.com/api/v1' + path, headers=headers,
                       data=None if body is None else json.dumps(body).encode(),
                       method='GET' if body is None else 'POST')
        try:
            response = opener.open(call, timeout=25)
        except HTTPError as error:
            response = error
        with response:
            envelope = json.load(response)
        if not 200 <= response.status < 300:
            code = envelope.get('error', {}).get('code', 'unknown')
            if not isinstance(code, str) or not 1 <= len(code) <= 80 or any(c not in 'abcdefghijklmnopqrstuvwxyz0123456789_' for c in code): code = 'unknown'
            raise ApiFailure('API HTTP ' + str(response.status) + ': ' + code)
        return envelope.get('data')
    if not sandbox:
        sources = api('/sources')
        if not isinstance(sources, list): raise ValueError('Invalid discovery response.')
        for source_id in source_ids:
            found = [source for source in sources if isinstance(source, dict) and source.get('id') == source_id]
            if len(found) != 1: raise ValueError('Required source missing or ambiguous.')
            source = found[0]
            if (source.get('availability') != 'available' or source.get('fresh') is not True or
                    not isinstance(source.get('capabilities'), list) or
                    request['subject']['entityType'] not in source['capabilities']):
                raise ValueError('Required source unavailable or incompatible.')
    result = api('/screenings', request)
    matches, coverage = result.get('matches'), result.get('coverage')
    if (not isinstance(result.get('id'), str) or not result['id'] or
            result.get('environment') != ('sandbox' if sandbox else 'production') or
            result.get('status') not in ['potential_match', 'no_match'] or
            not isinstance(matches, list) or (result['status'] == 'no_match') != (len(matches) == 0) or
            not isinstance(coverage, list) or len(coverage) != len(source_ids) or
            any(len([item for item in coverage if item.get('sourceId') == source_id and item.get('fresh') is True]) != 1 for source_id in source_ids)):
        raise ValueError('Incomplete screening result.')
    # Store result['id'] with your customer reference; retrieve evidence with results:read.
    print(json.dumps({'id': result['id'], 'status': result['status'], 'candidateCount': len(matches),
                      'sources': [item['sourceId'] for item in coverage]}))

try:
    main()
except ApiFailure as error:
    print(str(error), file=sys.stderr)
    sys.exit(1)
except Exception as error:
    print(str(error) if str(error) == 'Required source unavailable or incompatible.' else
          'Screening incomplete. Check credentials, request and source coverage.', file=sys.stderr)
    sys.exit(1)

Run the client

Run the sandbox request
python screen.py request.json

Confirm UK sanctions scope before production

Select uk-sanctions for the customer portfolio rather than using a legacy feed label as an API identifier. Keep the FCDO list reference with each job. Portfolio completion means every required subject reached a known state; it does not mean every customer was cleared.

Production source-discovery contract
CheckRequired evidence
GET /api/v1/sourcesProduction key with sources:read; response data array
iduk-sanctions
availability / freshavailable / true
capabilitiesContains the requested subject.entityType
currentVersionActive version; retrieved_at and last_checked_at are not publication dates
lastImportMay describe an import that is not the active version
Missing or ineligible sourceInspect disabledReason; required coverage stays incomplete
Displayed clientPerforms production discovery before screening
Read source discovery with your production key
curl --fail-with-body --max-time 25 --header "Authorization: Bearer $SANCTIONSKIT_API_KEY" "https://www.sanctionskit.com/api/v1/sources"

Select uk-sanctions in the production request

Explicit change from the sandbox exercise
SettingProduction request
KeySeparate production credential with screenings:write
Coveragesources: ["uk-sanctions"]; omit package
Operation IDNew ID for the changed body and environment
UsageProduction access and screening allowance apply
SubjectOnly lawfully held, known identifying facts; no invented date precision
request.json: explicit uk-sanctions selection
{
  "subject": {
    "name": "Mira Calder",
    "entityType": "person",
    "birthDate": "1984"
  },
  "sources": [
    "uk-sanctions"
  ],
  "retention": "standard"
}

Interpret UK sanctions evidence in this workflow

Retain the original CSV row reference alongside data.id and data.coverage. Show UK names, partial birth dates and designation context without filling missing date parts. Two equal names are not a valid join key between queued customers or returned candidates.

UK sanctions candidate fields to retain
Fields in data.matches[]Review use
record.id and record.sourceIdRetain the UK record reference and source identity when connecting the candidate to your customer.
record.names and birthDatesCompare original names and partial dates without inventing missing days or resolving similar names automatically.
record.designationsDisplay the available programs, legal references, effective dates and published wording beside the identity evidence.
  • data.id; data.coverage; data.versions; supplied disclaimer
  • data.matches[]: record, assessment, evidence, conflicts
  • GET /api/v1/results/{id}: results:read scope
  • Retained evidence and case review

Verify the application behavior

Recovery rules for the same saved operation
ConditionNext action
401 / 403Correct credentials or scopes before retrying
coverage_unavailable / stale_coverageKeep required coverage incomplete; no silent source substitution
Timeout after submissionRecover with the original Idempotency-Key and identical body
409Investigate an operation-key conflict
429 / 503Follow the specific error and Retry-After; an allowance cap may require a plan or usage change
Contract and safe diagnostic references
Operational reference
ReferenceUse
error.code / error.requestIdSafe recovery diagnostics; never log the submitted subject, key or full candidate response
potential_match / no_matchCompleted comparison outcomes; neither is an automatic business approval
  • Reconcile two identically named synthetic customers by their internal references.
  • Keep a year-only birth date unchanged during CSV-to-JSON conversion.
  • Mark malformed input rows separately from source or HTTP failures.

Sources and references

Frequently asked questions

Does the Python UK tutorial import the government list from CSV?

No. CSV may be your customer input, but the Python 3.11 worker sends subjects to SanctionsKit. The source-discovery response identifies the selected government-list coverage; your customer import is a separate preparation step.

Why is a customer row reference necessary for UK screening in Python?

Names are not unique identifiers. Carry an internal row or customer reference through the operation and result write so two people with equal names cannot exchange screening evidence during portfolio reconciliation.

What should my Python worker retain from a UK sanctions candidate?

Keep the UK source and record reference, original names, partial dates, and available designation details. These let a reviewer compare the evidence with your customer facts. Do not invent a full birth date to fill missing information.

Should a Python import fill missing UK candidate birth-date parts?

No. Keep the supplied precision visible. A year or month does not establish a specific day, and manufacturing a full date can make later identity comparisons look more certain than the evidence allows.