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.

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.
| Requirement | Value or action |
|---|---|
| Runtime | Use 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 file | Save the client as src/main.rs |
| 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"
}Add the project configuration
[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.
// 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
cargo run -- request.jsonConfirm 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.
| 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 | Add these discovery checks before its production call |
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
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.
| 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 |
- 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
Authority and technical sources
SanctionsKit documentation
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.