Developer guide
OFAC API in Python: customer screening workers
Run an OFAC API check in Python without extra packages. Use the standard library, preserve retry keys, and save SDN evidence for customer review.

On this page
Screen OFAC subjects in a Python worker
Keep input preparation, the HTTP call and result persistence as separate functions in your application. A worker should load the already-created operation rather than invent a new request key each time it starts. This makes a queue retry a recovery of the same comparison instead of an accidental additional screening.
The urllib example performs blocking network work. If you call it from a web application, choose an appropriate worker or request budget rather than allowing a long provider call to occupy an uncontrolled number of application workers. Set explicit limits and preserve incomplete operations for recovery.
Prepare the synthetic exercise
Start the Python worker with a saved synthetic job for Mira Calder and the sandbox@1 package. Put the operation reference in your queue, not a new request key generated inside the retry loop. This lets you test a worker restart without submitting real customer information.
| 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
urllib performs a blocking request in this example. Give the worker a bounded job budget and store its result before acknowledging the queue item. The production path checks source availability first, then validates that the returned coverage contains the requested SDN source.
"""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 OFAC scope before production
The worker selects ofac-sdn for each queued subject. Adding an OFAC non-SDN check is a coverage-policy change, not a retry of the SDN request. Persist the new source set with a new operation so the original job still explains which list it checked.
| Check | Required evidence |
|---|---|
| GET /api/v1/sources | Production key with sources:read; response data array |
| id | ofac-sdn |
| 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 ofac-sdn in the production request
| Setting | Production request |
|---|---|
| Key | Separate production credential with screenings:write |
| Coverage | sources: ["ofac-sdn"]; 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": [
"ofac-sdn"
],
"retention": "standard"
}Interpret OFAC evidence in this workflow
Write data.id and the local job reference together. A completed job with data.matches may require human review even though the queue item can be acknowledged. Preserve data.coverage and the year-only birth date; a queue serializer must not turn 1984 into an invented full date.
| Fields in data.matches[] | Review use |
|---|---|
| record.sourceId and record.id | Keep the SDN source and stable record reference attached to the candidate. |
| record.designations[].programs and legalReferences | Show the listed program and legal references; do not turn every non-SDN restriction into an asset freeze. |
| record.names, identifiers and birthDates | Compare the supplied identity with aliases and available identifiers, preserving partial-date precision. |
- 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 |
- Restart the worker after an accepted response but before acknowledgement.
- Submit the identical saved body and key during recovery.
- Verify that a year-only birth date survives queue serialization unchanged.
Sources and references
Authority and technical sources
SanctionsKit documentation
Frequently asked questions
Is the Python OFAC worker asynchronous?
The displayed Python 3.11 client uses blocking urllib calls and no pip dependency. Run it in a suitable worker. Putting this code inside an async function does not remove the blocking network operation.
Does changing an OFAC source set count as the same Python job?
It changes the screening request. Create a new persisted operation when moving from SDN-only coverage to a broader selection, and keep the earlier result linked to the job that actually produced it.
Should a Python queue retry create a new OFAC screening key?
No. Load the saved operation and use its original key and body. A fresh key starts another screening. Keep input preparation, the request, and result storage separate so the worker can recover after a restart.
When should the Python worker acknowledge an OFAC queue item?
After recording the completed screening reference or a durable failure state under your queue policy. Do not acknowledge a lost result as no_match; a timeout needs recovery before the business workflow can rely on it.