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();
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey | String | required | API key, sent as X-Api-Key. |
baseUrl | String | https://api.9thsense.ai | Base URL of the 9thSense API. |
tenantId | String | "" | Tenant identifier propagated on every request. |
timeout | Duration | Duration.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 class | When thrown |
|---|---|
AuthException | API key missing, invalid, or revoked (HTTP 401/403) |
CaseNotFoundException | Unknown case ID passed to get() / upload() |
VerificationException | A verification ran but returned an error result |
CaseTimeoutException | awaitCompletion exceeded its timeout |
SignatureVerificationException | Webhook 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");
}
}