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.

250 free live screenings. One lifetime allowance. No credit card required.
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.

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

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: 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 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.

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

Explicit change from the sandbox exercise
SettingProduction request
KeySeparate production credential with screenings:write
Coveragesources: ["ofac-sdn"]; 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 ofac-sdn selection
{
  "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.

OFAC candidate fields to retain
Fields in data.matches[]Review use
record.sourceId and record.idKeep the SDN source and stable record reference attached to the candidate.
record.designations[].programs and legalReferencesShow the listed program and legal references; do not turn every non-SDN restriction into an asset freeze.
record.names, identifiers and birthDatesCompare 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

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
  • 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

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.