Developer guide

OFAC API in Rust: screening workers

Call the OFAC API from a Rust worker with a blocking HTTP client. Keep request failures separate from SDN matches and completed no-match results.

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

Screen OFAC subjects in a Rust worker

The displayed program uses reqwest’s blocking client and suits a trusted CLI or blocking worker. A service running an async executor needs a deliberately adapted boundary; do not copy blocking network work into an async task and assume it has become nonblocking. Follow the selected reqwest version’s client documentation.

Whichever model you choose, persist the operation key and payload before sending. Keep transport, HTTP and response-contract failures distinguishable from the two completed screening outcomes. A returned Err is incomplete work and should never be converted into an empty result to simplify the caller.

Prepare the synthetic exercise

Use the provided Cargo configuration to run a blocking worker against a synthetic SDN-style workflow. Inject a malformed response in your local HTTP fixture and confirm the program returns an error. Empty candidate arrays are reserved for validated completed results, not failed parsing.

Rust setup and request files
RequirementValue or action
RuntimeUse a maintained Rust toolchain with Cargo. Save the client as src/main.rs and use the Cargo.toml shown below. This is a blocking CLI or worker example, not a function to run directly inside an async executor task.
Client fileSave the client as src/main.rs
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"
}

Add the project configuration

Cargo.toml
[package]
name = "sanctions-screen"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde_json = "1"

Implement the Rust request boundary

This reqwest implementation is blocking. Keep it in a CLI or an explicitly bounded blocking worker; an async application needs a deliberate integration boundary. The response validator, rather than successful deserialization alone, decides whether the screening contract is complete.

Rust: authenticated screening client
// Trusted CLI/backend worker. Do not run blocking I/O inside an async executor task.
use reqwest::{blocking::Client, redirect::Policy};
use serde_json::{json, Value};
use std::{env, error::Error, fs, io::Read, time::Duration};

fn run() -> Result<(), Box<dyn Error>> {
    let key = env::var("SANCTIONSKIT_API_KEY")?;
    let operation = env::var("SCREENING_OPERATION_ID")?;
    if operation.is_empty() { return Err("missing operation ID".into()); }
    let path = env::args().nth(1).unwrap_or_else(|| "request.json".to_owned());
    let raw = fs::read(path)?;
    if raw.len() > 65_536 { return Err("request file exceeds 64 KiB".into()); }
    let body: Value = serde_json::from_slice(&raw)?;
    if !body.is_object() || body.get("subject").is_none() || body.get("sources").is_some() == body.get("package").is_some() {
        return Err("invalid request object".into());
    }
    let client = Client::builder().connect_timeout(Duration::from_secs(10))
        .timeout(Duration::from_secs(25)).redirect(Policy::none()).build()?;
    let response = client.post("https://www.sanctionskit.com/api/v1/screenings")
        .bearer_auth(&key).header("Idempotency-Key", operation).json(&body).send()?;
    let status_code = response.status();
    let limit = 8 * 1024 * 1024;
    let mut payload = Vec::new();
    response.take((limit + 1) as u64).read_to_end(&mut payload)?;
    if payload.len() > limit { return Err("oversized response".into()); }
    if !status_code.is_success() { return Err("screening HTTP request incomplete".into()); }
    let envelope: Value = serde_json::from_slice(&payload)?;
    let data = envelope.get("data").ok_or("missing response data")?;
    let id = data.get("id").and_then(Value::as_str).filter(|s| !s.is_empty()).ok_or("missing screening ID")?;
    let status = data.get("status").and_then(Value::as_str).ok_or("missing screening status")?;
    let matches = data.get("matches").and_then(Value::as_array).ok_or("missing matches array")?;
    if !["potential_match", "no_match"].contains(&status) ||
        (status == "no_match" && !matches.is_empty()) || (status == "potential_match" && matches.is_empty()) {
        return Err("inconsistent screening response".into());
    }
    println!("{}", json!({"id":id,"status":status,"candidateCount":matches.len()}));
    Ok(())
}
fn main() {
    if run().is_err() {
        // Do not print parser errors, submitted identity, or the raw response.
        eprintln!("Screening incomplete. Check the operation before retrying.");
        std::process::exit(1);
    }
}

Run the client

Run the sandbox request
cargo run -- request.json

Confirm OFAC scope before production

The Rust request fixes coverage to ofac-sdn after source discovery has confirmed eligibility. Do not replace an unavailable SDN source with a convenient list or infer that a non-SDN entry has the same effect. Source selection belongs alongside the persisted operation, not inside a fallback branch.

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 clientAdd these discovery checks before its production call
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

Represent transport failure, completed no_match and potential_match as separate application states. Carry data.coverage and data.id into the retained review record. Returning an empty vector from an error branch would erase the distinction between failed work and a completed SDN comparison.

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
  • Supply malformed JSON and confirm no completed result is emitted.
  • Simulate unavailable SDN coverage without switching to another source.
  • Verify safe diagnostics contain no submitted name or raw candidate body.

Sources and references

Frequently asked questions

Can the blocking Rust OFAC client run directly on an async executor?

Do not place blocking reqwest work directly on an async executor and assume it is nonblocking. Use an appropriate worker boundary or deliberately adapt the client while preserving cancellation, validation and recovery semantics.

Should a Rust OFAC client turn a parse error into an empty vector?

No. An empty result can mean a completed no-match check only after validating the response. Parsing, HTTP and coverage failures must remain separate error states that the caller cannot confuse with clearance.

What should the Rust worker log after an OFAC API failure?

Keep diagnostic output to a safe error code, HTTP status, and request ID when available. Store the operation privately for recovery. Do not print the submitted name, API key, or full candidate evidence to routine worker logs.

What is a useful local failure fixture for the Rust SDN worker?

Return an HTTP success with the wrong coverage or an invalid matches shape. The worker should reject the response and retain the operation for investigation rather than emit a completed screening summary.