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.
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.
| Requirement | Value or action |
|---|---|
| Runtime | 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. |
| Client file | Save the client as screen.py. |
| Screening endpoint | POST /api/v1/screenings |
| SANCTIONSKIT_API_KEY | Sandbox key; screenings:write and results:read scopes |
| SCREENING_OPERATION_ID | Persist one operation ID; reuse only with the identical request body |
| request.json | Mira Calder: invented subject; sandbox@1: synthetic records |
{
"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 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
python screen.py request.jsonConfirm 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.
| Check | Required evidence |
|---|---|
| GET /api/v1/sources | Production key with sources:read; response data array |
| id | eu-consolidated |
| availability / fresh | available / true |
| capabilities | Contains the requested subject.entityType |
| currentVersion | Active version; retrieved_at and last_checked_at are not publication dates |
| lastImport | May describe an import that is not the active version |
| Missing or ineligible source | Inspect disabledReason; required coverage stays incomplete |
| Displayed client | Performs production discovery before screening |
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
| Setting | Production request |
|---|---|
| Key | Separate production credential with screenings:write |
| Coverage | sources: ["eu-consolidated"]; omit package |
| Operation ID | New ID for the changed body and environment |
| Usage | Production access and screening allowance apply |
| Subject | Only lawfully held, known identifying facts; no invented date precision |
{
"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.
| Fields in data.matches[] | Review use |
|---|---|
| record.id and record.sourceId | Save the EU record identity with the screening result instead of indexing the returned names yourself. |
| record.names, identifiers and birthDates | Show primary names, aliases and available identifiers; keep a year or range as published. |
| record.designations[].legalReferences and programs | Read 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
| Condition | Next action |
|---|---|
| 401 / 403 | Correct credentials or scopes before retrying |
| coverage_unavailable / stale_coverage | Keep required coverage incomplete; no silent source substitution |
| Timeout after submission | Recover with the original Idempotency-Key and identical body |
| 409 | Investigate an operation-key conflict |
| 429 / 503 | Follow the specific error and Retry-After; an allowance cap may require a plan or usage change |
Contract and safe diagnostic references
| Reference | Use |
|---|---|
| error.code / error.requestId | Safe recovery diagnostics; never log the submitted subject, key or full candidate response |
| potential_match / no_match | Completed 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
Authority and technical sources
SanctionsKit documentation
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.
