Install, configure, and use the 9thSense Java SDK — CompletableFuture-based, Java 17+

Java SDK


The ninthsense-sdk Java SDK is the official client for running 9thSense verifications and managing cases from JVM applications. All network calls return CompletableFuture<T> so they compose cleanly with reactive and virtual-thread architectures. The SDK requires Java 17 or later.

Every verification result carries the check verdicts — liveness, deepfake, face match, document integrity — and the Goal Rule outcomes that decided it.


Installation

Maven

Add the dependency to your pom.xml:

<dependency>
  <groupId>ai.ninthsense</groupId>
  <artifactId>ninthsense-sdk</artifactId>
  <version>0.1.0</version>
</dependency>

Gradle

dependencies {
    implementation 'ai.ninthsense:ninthsense-sdk:0.1.0'
}

Gradle (Kotlin DSL)

dependencies {
    implementation("ai.ninthsense:ninthsense-sdk:0.1.0")
}

Client

The Client class is the entry point for all SDK calls. Build it with the fluent Client.Builder.

Constructor

var client = new Client.Builder()
    .apiKey(System.getenv("NINTHSENSE_API_KEY"))   // required
    .baseUrl("https://api.9thsense.ai")             // default
    .tenantId("your-tenant-uuid")                   // optional
    .timeout(Duration.ofSeconds(60))                // default: 60s
    .build();
ParameterTypeDefaultDescription
apiKeyStringrequiredAPI key, sent as X-Api-Key.
baseUrlStringhttps://api.9thsense.aiBase URL of the 9thSense API.
tenantIdString""Tenant identifier propagated on every request.
timeoutDurationDuration.ofSeconds(60)Per-request HTTP timeout.

try-with-resources (recommended)

Client implements AutoCloseable and holds an HTTP connection pool. Use try-with-resources to guarantee cleanup:

try (var client = new Client.Builder()
        .apiKey(System.getenv("NINTHSENSE_API_KEY"))
        .build()) {

    var health = client.healthz().join();
    System.out.println(health); // {status=ok}
}

Async composition

All methods return CompletableFuture<T>. Call .join() to block, or compose with .thenApply() / .thenCompose():

client.verify()
    .document(base64Image, "pan_card.jpg", "kyc_pan_card")
    .thenApply(VerifyResult::extracted)
    .thenAccept(fields -> System.out.println("PAN: " + fields.get("pan_number")))
    .join();

Cases namespace

A Case is one verification run for one subject, executed against a deployed agent. All case operations go through client.cases().

create / upload / wait

import ai.ninthsense.cases.CaseInfo;
import java.nio.file.Files;
import java.nio.file.Path;

CaseInfo kase = client.cases()
    .create("kyc_individual")                       // agent goal
    .join();

byte[] pan = Files.readAllBytes(Path.of("pan_card.jpg"));
byte[] selfie = Files.readAllBytes(Path.of("selfie.jpg"));

client.cases().upload(kase.sessionId(), pan, "pan_card.jpg").join();       // auto-classified
client.cases().upload(kase.sessionId(), selfie, "selfie.jpg", "selfie").join();

CaseInfo terminal = client.cases()
    .awaitCompletion(
        kase.sessionId(),
        Duration.ofSeconds(2),    // poll interval
        Duration.ofMinutes(3)     // timeout
    )
    .join();

System.out.println(terminal.status());   // completed | review | denied

Throws CaseTimeoutException (unchecked) if the timeout is exceeded.

Reading check verdicts and goal progress

for (CheckResult check : terminal.checks()) {
    System.out.printf("%s: %s (%.2f)%n",
        check.name(), check.verdict(), check.confidence());
}
// liveness: pass (0.97)
// deepfake: pass (0.99)
// face_match: pass (0.91)

for (RuleOutcome rule : terminal.goalProgress()) {
    System.out.printf("rule %s: %s%n",
        rule.ruleId(), rule.passed() ? "PASS" : "FAIL");
}

CaseInfo record

public record CaseInfo(
    String id,
    String status,                        // collecting | processing | review | completed | denied
    Map<String, Object> extracted,
    List<CheckResult> checks,
    List<RuleOutcome> goalProgress
) {}

CheckResult record

public record CheckResult(
    String name,                          // "liveness", "deepfake", "face_match", ...
    String verdict,                       // pass | fail | flag
    double confidence,
    List<Map<String, Object>> markers     // timestamped markers for video checks
) {}

RuleOutcome record

public record RuleOutcome(
    String ruleId,
    boolean passed,
    String severity,                      // hard_stop | require
    String message
) {}

get / list

// Single case
CaseInfo kase = client.cases().get(caseId).join();

// Recent cases
List<CaseInfo> recent = client.cases().list(50).join();

One-shot verification

For flows that don't need a multi-document case, client.verify() runs extraction plus the configured checks on a single submission.

document

import ai.ninthsense.verify.VerifyResult;
import java.util.Base64;

String base64 = Base64.getEncoder().encodeToString(pdfBytes);

VerifyResult result = client.verify()
    .document(base64, "statement_jan2026.pdf", "fin_bank_statement")
    .join();

System.out.println(result.extracted());   // typed fields
System.out.println(result.checks());      // e.g. document integrity verdict
System.out.println(result.rules());       // goal-rule outcomes

selfie

Runs liveness, deepfake detection, and — when a reference ID photo is named — face match:

VerifyResult result = client.verify()
    .selfie(base64Selfie, "kyc_pan_card.photo")
    .join();

VerifyResult record

public record VerifyResult(
    boolean success,
    Map<String, Object> extracted,
    List<CheckResult> checks,
    List<RuleOutcome> rules,
    String error,
    long latencyMs,
    boolean fromSelfHosted
) {}

Faces namespace

Direct biometric operations, mirroring the Face Biometrics API:

// 1:1 likeness
FaceMatch match = client.faces().match(selfieBase64, idPhotoBase64).join();
System.out.println(match.score() + " " + match.verdict());

// 1:N search across configured databases
List<FaceHit> hits = client.faces().search(selfieBase64, 5).join();
for (FaceHit hit : hits) {
    System.out.println(hit.identityId() + " " + hit.database() + " " + hit.confidence());
}

Agents namespace

import ai.ninthsense.agents.AgentInfo;

// List all deployed agents
List<AgentInfo> agents = client.agents().list().join();

// Get by ID
AgentInfo agent = client.agents().get(agentId).join();

Webhooks

Verify signed events delivered by 9thSense to your endpoint.

verifySignature

import ai.ninthsense.webhooks.Webhooks;

byte[] rawBody = request.body();
String checksum = request.header("X-Digio-Checksum");
String timestamp = request.header("X-Digio-Timestamp");
long ts = Long.parseLong(timestamp);

boolean valid = Webhooks.verifySignature(rawBody, checksum, secret, ts);
if (!valid) { response.status(400); return; }

constructEvent

Verify signature, check timestamp freshness, and parse the JSON envelope in one call:

import ai.ninthsense.webhooks.WebhookEvent;
import ai.ninthsense.errors.SignatureVerificationException;

try {
    WebhookEvent event = Webhooks.constructEvent(
        rawBody,
        Map.of(
            "X-Digio-Checksum",  request.header("X-Digio-Checksum"),
            "X-Digio-Timestamp", request.header("X-Digio-Timestamp")
        ),
        System.getenv("NINTHSENSE_WEBHOOK_SECRET"),
        Duration.ofMinutes(5),   // timestamp tolerance
        Clock.systemUTC()
    );

    if ("case.completed".equals(event.type())) {
        String caseId  = (String) event.data().get("case_id");
        String verdict = (String) event.data().get("verdict");
        // update your DB, trigger downstream actions ...
    }
} catch (SignatureVerificationException e) {
    response.status(400);
    return;
}

Error handling

All SDK exceptions extend NinthSenseException (unchecked). When a CompletableFuture fails, the exception is wrapped in a CompletionException — unwrap it with .getCause().

Exception classWhen thrown
AuthExceptionAPI key missing, invalid, or revoked (HTTP 401/403)
CaseNotFoundExceptionUnknown case ID passed to get() / upload()
VerificationExceptionA verification ran but returned an error result
CaseTimeoutExceptionawaitCompletion exceeded its timeout
SignatureVerificationExceptionWebhook signature or timestamp invalid
try {
    CaseInfo kase = client.cases().create("kyc_individual", null, "LOAN-2291").join();
} catch (java.util.concurrent.CompletionException ex) {
    Throwable cause = ex.getCause();
    if (cause instanceof ai.ninthsense.errors.AuthException) {
        // revoke key, alert ops — do not retry
    } else if (cause instanceof ai.ninthsense.errors.VerificationException v) {
        System.err.println("Verification error: " + v.getMessage());
    } else {
        throw ex;   // re-throw unexpected errors
    }
}

LocalSimulator

LocalSimulator is a drop-in ClientInterface replacement that returns canned responses without any network calls. Use it in unit tests. Simulated cases progress instantly to completed with passing check verdicts (liveness 0.97, deepfake 0.99, face_match 0.91) and all goal rules passed.

import ai.ninthsense.ClientInterface;
import ai.ninthsense.LocalSimulator;

ClientInterface client = new LocalSimulator();

// Returns a canned completed case — no network
CaseInfo kase = client.cases().create("kyc_individual", null, "LOAN-2291").join();
client.cases().upload(kase.sessionId(), sampleBytes, "pan_card.jpg").join();
CaseInfo done = client.cases().awaitCompletion(
        kase.sessionId(), Duration.ofMillis(1), Duration.ofSeconds(1)).join();

assert "completed".equals(done.status());
assert done.goalProgress().stream().allMatch(RuleOutcome::passed);

LocalSimulator implements the same ClientInterface as the real Client, so you can inject it via constructor in production code without changing signatures:

// Production
ClientInterface client = new Client.Builder()
    .apiKey(apiKey)
    .build();

// Test
ClientInterface client = new LocalSimulator();

// Same interface — no code changes needed
service.runKyc(client, docBytes, selfieBytes);

Spring Boot integration

Declare the client as a singleton bean and inject it wherever needed:

@Configuration
public class NinthSenseConfig {

    @Bean
    public ClientInterface ninthSenseClient(
            @Value("${ninthsense.api-key}") String apiKey,
            @Value("${ninthsense.base-url:https://api.9thsense.ai}") String baseUrl) {
        return new Client.Builder()
            .apiKey(apiKey)
            .baseUrl(baseUrl)
            .timeout(Duration.ofSeconds(30))
            .build();
    }
}
@Service
public class KycService {

    private final ClientInterface client;

    public KycService(ClientInterface client) {
        this.client = client;
    }

    public CompletableFuture<VerifyResult> verifyPan(byte[] imageBytes) {
        String base64 = Base64.getEncoder().encodeToString(imageBytes);
        return client.verify()
            .document(base64, "pan_card.jpg", "kyc_pan_card");
    }
}

In tests, pass a LocalSimulator directly:

@SpringBootTest
class KycServiceTest {

    @Test
    void verifiesPan() {
        var service = new KycService(new LocalSimulator());
        var result = service.verifyPan(sampleImageBytes()).join();
        assertThat(result.success()).isTrue();
        assertThat(result.extracted()).containsKey("pan_number");
    }
}