Developer guide

OFAC API in Java: customer onboarding

Add OFAC API checks to a Java onboarding service. Run the Java 21 example with Jackson, then save SDN results and handle retries without duplicate checks.

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

Screen OFAC subjects in a Java service

Keep the JDK HttpClient call and Jackson response validation behind a small service boundary. The application supplies a validated subject, selected coverage and a persisted operation key. It receives a completed screening reference or an explicit failure path, while customer eligibility remains in the surrounding business workflow.

For a server application, plan the request and worker budget before choosing synchronous or asynchronous execution. The example is a complete CLI request, not a throughput benchmark. Bound concurrent operations and review the batch API when the business task is a portfolio run.

Prepare the synthetic exercise

Build the Java 21 example with the supplied Maven file and submit the fictional sandbox person. Test the boundary between Jackson decoding and business state: a structurally valid response still needs the expected status, source coverage and result identity before onboarding can use it.

Java setup and request files
RequirementValue or action
RuntimeUse 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.
Client fileSave the client as src/main/java/Screen.java
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

pom.xml
<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>

Implement the Java request boundary

Keep JDK HttpClient transport and Jackson validation inside a small service adapter. The adapter should return a screening reference or an explicit incomplete state. Choosing synchronous versus asynchronous execution changes resource use, not the requirement to persist the operation before sending it.

Java: authenticated screening client
// 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");
        byte[] raw = Files.readAllBytes(Path.of(args.length > 0 ? args[0] : "request.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 the sandbox request
mvn compile exec:java -Dexec.args="request.json"

Confirm OFAC scope before production

Use ofac-sdn for the demonstrated Java onboarding check. If your policy covers non-SDN lists or separate ownership controls, represent those requirements explicitly rather than assuming a successful SDN response satisfies them. Discover the required source before the production call.

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

Join data.id to the customer and saved operation in one application update. data.coverage and candidate evidence belong to the screening record, while an analyst’s disposition belongs to the review workflow. Keeping those objects separate prevents HTTP success from becoming an accidental approval.

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
  • Deserialize a response without the required source coverage and reject it.
  • Persist a potential match while keeping the customer in pending review.
  • Recover a timeout without generating a new operation inside the service adapter.

Sources and references

Frequently asked questions

What does the Java OFAC example use Jackson for?

Jackson reads and inspects the JSON response in the Java 21 example. Parsing does not prove completion: the adapter also needs a successful HTTP response and the expected screening status, coverage and identity fields.

Can the Java customer model use one boolean for OFAC approval?

That would conflate the screening result with the business decision. Keep completed comparison, possible matches, incomplete work and analyst disposition distinguishable so a successful API call does not approve the customer.

What should a Java onboarding service save from an OFAC result?

Save the screening ID with your customer reference and operation ID. Keep coverage and versions available to the authorized review flow. Separate a completed comparison from an unresolved case so the customer does not advance merely because HTTP succeeded.

Is the Java OFAC sample a performance benchmark?

No. It demonstrates a complete CLI request and response checks. Measure concurrency and latency for your own workload, and consider the batch API when you need durable progress for a portfolio.