Developer guide
OFAC API in Java: screening and error handling
Build OFAC screening in Java with a complete HTTP example, explicit source selection, idempotency, safe failures, and review handoffs.
On this page
What this Java integration does
OFAC is the U.S. Treasury authority that publishes the SDN and non-SDN sanctions data. This tutorial calls SanctionsKit, not an OFAC-operated screening endpoint. Keep the original authority and list identity visible in each result.
The client submits a subject, validates the response, and prints only the screening ID, status, and candidate count. It does not approve a customer, resolve an identity, or make a legal decision. A nonzero exit leaves the local operation incomplete.
Prepare the runtime and a synthetic request
Use Java 21 and Maven. Save the client as src/main/java/Screen.java and the pom.xml shown below at the project root. The example uses the JDK HTTP client and Jackson 2.x for JSON tree validation.
Save the following as request.json. Mira Calder is an invented subject and sandbox@1 uses synthetic fixtures, not a current production sanctions search. Set SANCTIONSKIT_API_KEY to your test key in the process environment. Set SCREENING_OPERATION_ID to a new persistent identifier for this operation, and reuse it only for an unchanged retry.
{
"subject": {
"name": "Mira Calder",
"entityType": "person",
"birthDate": "1984"
},
"package": "sandbox@1",
"retention": "standard"
}Add the project configuration
Save this as pom.xml. These dependencies are for the standalone tutorial, not dependencies to add to SanctionsKit’s website build. Review and lock them under your own maintenance process.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion><groupId>example</groupId><artifactId>sanctions-screen</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>21</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.18.10</version></dependency></dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version></plugin>
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.5.0</version><configuration><mainClass>Screen</mainClass></configuration></plugin>
</plugins></build>
</project>Send the request and validate the result
Save the client as src/main/java/Screen.java. The endpoint is fixed to SanctionsKit’s HTTPS API. The example rejects redirects, requires explicit authorization before using a live key, and does not retry automatically. Production integrations should preserve the operation state before the first network call.
// Java 21+, Jackson 2.x. Backend or trusted CLI. See the adjacent pom.xml.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public final class Screen {
public static void main(String[] args) {
try { run(args); }
catch (Exception error) {
// Parser exceptions may contain excerpts, so do not print their messages.
System.err.println("Screening incomplete. Check the operation before retrying.");
System.exit(1);
}
}
static void run(String[] args) throws Exception {
String key = System.getenv("SANCTIONSKIT_API_KEY");
String operation = System.getenv("SCREENING_OPERATION_ID");
if (key == null || operation == null || operation.isBlank()) throw new IllegalArgumentException("Missing configuration");
if (!key.startsWith("sk_test_") && !(key.startsWith("sk_live_") && "yes".equals(System.getenv("ALLOW_LIVE_SCREENING")))) {
throw new IllegalArgumentException("Live operation not explicitly authorized");
}
byte[] raw = Files.readAllBytes(Path.of(args.length > 0 ? args[0] : "../request.sandbox.json"));
if (raw.length > 65536) throw new IllegalArgumentException("Oversized request");
ObjectMapper mapper = new ObjectMapper();
JsonNode body = mapper.readTree(raw);
if (body == null || !body.isObject() || !body.has("subject") || body.has("sources") == body.has("package")) {
throw new IllegalArgumentException("Invalid request object");
}
HttpRequest request = HttpRequest.newBuilder(URI.create("https://www.sanctionskit.com/api/v1/screenings"))
.timeout(Duration.ofSeconds(25))
.header("Authorization", "Bearer " + key).header("Content-Type", "application/json")
.header("Idempotency-Key", operation).POST(HttpRequest.BodyPublishers.ofByteArray(raw)).build();
try (HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NEVER).build()) {
// A file subscriber avoids buffering the complete body in memory.
Path temp = Files.createTempFile("sanctions-response-", ".json");
try {
var future = client.sendAsync(request, HttpResponse.BodyHandlers.ofFile(temp));
HttpResponse<Path> response;
try { response = future.get(30, TimeUnit.SECONDS); }
catch (Exception error) { future.cancel(true); throw error; }
if (response.statusCode() < 200 || response.statusCode() >= 300 || Files.size(temp) > 8 * 1024 * 1024) {
throw new IllegalStateException("Incomplete HTTP response");
}
JsonNode envelope = mapper.readTree(temp.toFile());
JsonNode data = envelope == null ? null : envelope.get("data");
if (data == null || !data.isObject() || !data.path("id").isTextual() || data.path("id").asText().isEmpty()
|| !data.path("status").isTextual() || !data.path("matches").isArray()) {
throw new IllegalStateException("Unexpected screening response");
}
String status = data.get("status").asText(); int count = data.get("matches").size();
if (!(status.equals("potential_match") || status.equals("no_match"))
|| (status.equals("no_match") && count != 0) || (status.equals("potential_match") && count == 0)) {
throw new IllegalStateException("Inconsistent screening response");
}
System.out.println(mapper.writeValueAsString(Map.of("id", data.get("id").asText(), "status", status, "candidateCount", count)));
} finally { Files.deleteIfExists(temp); }
}
}
}Run the client
Run this from the directory containing request.json and the project files. Keep the API key out of committed files and shell tracing. The tutorial’s request-file limit is 64 KiB and its response parsing limit is 8 MiB; these are client safeguards, not claims about API service limits.
Validate JsonNode types before converting values; a missing field must not become a default empty string or zero. The example rejects redirects and uses a time-bounded asynchronous file download before parsing. The size check bounds parsing input, not a complete production temporary-storage quota.
mvn compile exec:java -Dexec.args="request.json"Change to the required production coverage
The production example selects ofac-sdn only. Add ofac-non-sdn only when your required scope includes it, after checking current availability and compatibility. An SDN-only check is not a complete non-SDN check, and non-SDN records do not all have the same restrictions.
For an authorized production operation, replace the package field with the sources array shown below, supply the actual lawfully held subject details, use a live key, and set ALLOW_LIVE_SCREENING=yes. Keep the complete request in request.json and create a new operation ID. Do not send both sources and package. The fragment below is a coverage change, not a complete request.
{
"sources": [
"ofac-sdn"
]
}Preserve the source details a reviewer needs
Preserve source record IDs, list names, program context, aliases, date precision, and the actual fields that support or conflict with a candidate. A no-match result does not resolve OFAC’s 50 Percent Rule, geography-based restrictions, or the legality of a proposed activity.
Retain the screening ID with your internal subject reference. Retrieve the complete evidence through the documented retained-result workflow when authorized; the console summary deliberately does not print raw identity details. A later analyst decision must not overwrite the original screening outcome.
Handle failures and test the source-specific edge cases
Treat HTTP errors, timeouts, malformed JSON, missing fields, unknown statuses, and unavailable required coverage as incomplete checks. A 401 or 403 needs credential or scope correction. A 409 needs idempotency-conflict handling. A 429 needs its specific rate-limit or usage-cap treatment; it is not a no-match result.
Test similar names with different reliable birth years, overlapping year-only dates, and a source record with no comparable identifier. The absence of an identifier is unknown, not a conflict.
Use a mock HTTP server to exercise malformed JSON, absent data, non-text statuses, and a non-array matches field. Pin and review dependencies in your project rather than automatically updating Jackson without compatibility tests.
- Verify both valid result states and inconsistent status/matches combinations.
- Retry an accepted operation only with its original key and unchanged payload.
- Keep missing identity facts unknown instead of inventing contradictory values.
- Route potential matches into review before the separate business decision.
- Keep secrets, submitted names, and raw error bodies out of routine logs.