Developer guide
U.S. CSL API in Rust: review component list evidence
Use the U.S. CSL API in a Rust trade-screening service. Preserve the component list behind each candidate so reviewers can check the actual restriction.

On this page
Review CSL candidates in a Rust service
A typed application model should preserve the screening ID and the source context needed to retrieve and interpret candidate evidence. Avoid a single sanctioned boolean that treats all Consolidated Screening List entries as equivalent. Component lists can raise different questions for a trade reviewer.
For synthetic fixtures, give two similarly named organizations different authority references. Verify that serialization and any local deduplication preserve both candidate records. Identity review can connect related evidence without discarding where each record originated.
Prepare the synthetic exercise
Make two local candidate fixtures with similar organization names but different component-list authorities. The blocking Rust client should preserve both after deserialization. A deduplication function keyed by normalized name would discard the very context a trade reviewer needs.
| 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 | Example Harbor Trading: invented subject; sandbox@1: synthetic records |
{
"subject": {
"name": "Example Harbor Trading",
"entityType": "organization"
},
"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
Use reqwest’s blocking client in the trusted worker shown here and keep your typed review model separate from transport. Represent the source record, list and authority explicitly. A generic Vec<String> of names is too narrow for the evidence this workflow must preserve.
// 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 U.S. CSL scope before production
The us-csl source combines lists whose restrictions differ. Rust’s type system can help keep that distinction visible, but it cannot turn the transport source into a legal conclusion. Retain the component list and inspect its authority before interpreting a potential match.
| Check | Required evidence |
|---|---|
| GET /api/v1/sources | Production key with sources:read; response data array |
| id | us-csl |
| 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 us-csl in the production request
| Setting | Production request |
|---|---|
| Key | Separate production credential with screenings:write |
| Coverage | sources: ["us-csl"]; 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": "Example Harbor Trading",
"entityType": "organization"
},
"sources": [
"us-csl"
],
"retention": "standard"
}Interpret U.S. CSL evidence in this workflow
Carry data.id and data.coverage with each candidate’s source context. If related authority records concern the same real-world organization, preserve both references rather than deleting one as a duplicate name. Identity comparison and restriction interpretation are separate review steps.
| Fields in data.matches[] | Review use |
|---|---|
| record.sourceId, record.list and record.authority | Distinguish the CSL transport source from the component list and issuing authority. |
| record.designations[].legalReferences and wording | Present the actual restriction context instead of a universal sanctioned flag. |
| record.id, names and addresses | Keep each party and candidate separately traceable, including address evidence relevant to an organization. |
- 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 |
- Deserialize two same-name candidate records without merging their authorities.
- Round-trip the review model and retain both original references.
- Keep a response-contract error distinct from an empty candidate vector.
Sources and references
Authority and technical sources
SanctionsKit documentation
Frequently asked questions
Why is a vector of names insufficient for the Rust CSL integration?
It drops the component-list and authority context behind each candidate. Keep the record reference and restriction evidence in the model so the reviewer can understand why the party appears in the consolidated source.
Can Rust deduplicate CSL records by normalized company name?
A name alone is not enough. Different authority records may concern related or distinct entities and carry different measures. Preserve their source references until reliable evidence supports an explicit relationship.
Does one Rust CSL request screen every party in a shipment?
No. A request checks the subject you supply. Screen the buyer, consignee, intermediaries, and other required parties separately under your policy. Keep each party reference with its own result so a completed buyer check cannot stand in for the rest.
What should the Rust CSL serialization test retain?
Use invented candidates with equal display names but different lists and authorities. Assert that both identities and their references survive serialization, rather than accepting a compact output that erases one record.