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.

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.
| 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 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 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 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.
| Check | Required evidence |
|---|---|
| GET /api/v1/sources | Production key with sources:read; response data array |
| id | uk-sanctions |
| 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 uk-sanctions in the production request
| Setting | Production request |
|---|---|
| Key | Separate production credential with screenings:write |
| Coverage | sources: ["uk-sanctions"]; 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": [
"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.
| Fields in data.matches[] | Review use |
|---|---|
| record.id and record.sourceId | Retain the UK record reference and source identity when connecting the candidate to your customer. |
| record.names and birthDates | Compare original names and partial dates without inventing missing days or resolving similar names automatically. |
| record.designations | Display 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
| 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 |
- 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
Authority and technical sources
SanctionsKit documentation
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.