Developer guide

EU sanctions API in Python: customer screening workers

Build a Python worker for the EU sanctions API’s financial list. Save each screening ID and compare the listed party with known customer details.

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

Screen EU customers from a Python worker

Give each queued customer a stable local operation ID and store the exact subject and selected source with the job. On completion, save the screening ID with that customer so a reviewer can retrieve the same evidence later.

Process each operation independently. An unavailable EU source leaves that job incomplete; it must not produce an empty candidate list or a completed customer check.

Prepare the synthetic exercise

Use a synthetic Python customer file containing year-only and full birth dates. Preserve each value’s precision in request.json and in the saved operation. A transformation that fills missing months or days would create identity evidence the customer source never supplied.

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 standard-library worker checks the selected EU source before sending the subject. Keep input conversion outside the HTTP function so date validation and network recovery can be tested independently. A failed job should retain its original input for diagnosis.

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 EU sanctions scope before production

The production source is eu-consolidated, the EU financial sanctions list. Store that specific scope with every customer operation. Its coverage does not extend automatically to all trade or sectoral restrictions, and its availability must be confirmed for the requested subject type.

Production source-discovery contract
CheckRequired evidence
GET /api/v1/sourcesProduction key with sources:read; response data array
ideu-consolidated
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 eu-consolidated in the production request

Explicit change from the sandbox exercise
SettingProduction request
KeySeparate production credential with screenings:write
Coveragesources: ["eu-consolidated"]; 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 eu-consolidated selection
{
  "subject": {
    "name": "Mira Calder",
    "entityType": "person",
    "birthDate": "1984"
  },
  "sources": [
    "eu-consolidated"
  ],
  "retention": "standard"
}

Interpret EU sanctions evidence in this workflow

Use data.id to retrieve the completed comparison and data.coverage to retain its version. Present partial birth dates, alternative names and regulation references at their supplied precision. A later source update cannot replace the evidence used by the earlier worker run.

EU sanctions candidate fields to retain
Fields in data.matches[]Review use
record.id and record.sourceIdSave the EU record identity with the screening result instead of indexing the returned names yourself.
record.names, identifiers and birthDatesShow primary names, aliases and available identifiers; keep a year or range as published.
record.designations[].legalReferences and programsRead the applicable regulation and regime context beside the candidate.
  • 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
  • Keep the invented birth year 1984 as a year through serialization.
  • Retain a failed EU job without converting its outcome to no_match.
  • Retrieve the saved result rather than substituting a later source snapshot.

Sources and references

Frequently asked questions

Can a Python EU import normalize every birth date to a full date?

Not when the original value is partial. Keep a supplied year or month at that precision. Inventing a day creates false specificity and can distort the reviewer’s identity comparison.

Which EU source should the Python operation retain?

Record eu-consolidated and the completed response’s coverage version with the customer job. A later source-catalog lookup describes current availability, not necessarily the data used by that earlier comparison.

How should a Python EU worker preserve year-only birth dates?

Keep a year-only value as a year. Do not add January 1 or another invented day. Show available aliases and identifiers with their original detail so the reviewer can judge the evidence without mistaking an estimated date for a known fact.

What does a successful synthetic EU worker run qualify?

It demonstrates local request, serialization and result handling against invented fixtures. It does not qualify live EU source access, current list contents, redistribution rights or the legal result for an actual customer.