Java SDK
This guide shows how to install the Java SDK (io.enterpriseai:enterprise-ai-java), connect it to the Control
Plane of the Navin Platform and call every resource group of API v1 with typed exceptions, retries, pagination and
streaming. Source: sdks/java/enterprise-ai-java/src/main/java/io/enterpriseai. Every class, method and field is
listed in the generated reference.
Version française : java.fr.md
Overview
EnterpriseAIClient is a thread safe client over java.net.http.HttpClient. It is built with
EnterpriseAIClient.builder() and exposes one accessor per resource group of API v1 (client.agents(),
client.gateway(), client.finops()...). Each method maps one documented route and returns a Jackson JsonNode,
a typed model (ChatResponse, ChatChunk) or a Page<JsonNode>. Errors are unchecked EaiException subclasses.
The artifact also contains io.enterpriseai.manager.ManagerClient for the Navin Manager
(Navin Manager SDK) and offline contract validation (io.enterpriseai.contracts.ContractValidator).
The separate artifact io.enterpriseai:enterprise-ai-spring is a Spring Boot starter.
Requirements and compatibility
| Item | Supported |
|---|---|
| Java | 17 or later (maven.compiler.release 17 in sdks/java/pom.xml) |
| Operating systems | Every platform with a Java 17 runtime (Linux, macOS, Windows) |
| Dependencies | jackson-databind, jackson-datatype-jsr310, json-schema-validator (networknt), slf4j-api |
| Spring Boot starter | enterprise-ai-spring, built against the spring-boot-dependencies BOM of the parent POM |
| Control Plane API | v1 |
Installation
-
Download the Maven repository from Harbor. The SDK packages are hosted in the
sdkproject of the Navinspire Harbor registry (artifactsdk/java, tag = SDK version). The Navinspire registry isregistry.navinspire.ai; log in with a robot account that can pull from thesdkproject (given by the platform team). If your organization mirrors thesdkproject in its own Harbor, use the address of that mirror instead.oras login registry.navinspire.aioras pull registry.navinspire.ai/sdk/java:1.0.2 -o sdk-javaExpected result: the
sdk-javafolder holds the Maven repository of the SDK (io/enterpriseai/...). -
Declare that folder as a repository, then add the dependency. Maven:
<repositories><repository><id>enterprise-ai-sdks</id><url>file://${project.basedir}/sdk-java</url></repository></repositories><dependency><groupId>io.enterpriseai</groupId><artifactId>enterprise-ai-java</artifactId><version>1.0.2</version></dependency>Gradle (Kotlin DSL):
repositories {maven { url = uri("sdk-java") }}dependencies {implementation("io.enterpriseai:enterprise-ai-java:1.0.2")}Expected result:
mvn dependency:tree(orgradle dependencies) listsio.enterpriseai:enterprise-ai-java:1.0.2.If it fails: check
java -version(17 or later) and the repositories of the build.
Offline install
The Maven repository pulled from Harbor is a local folder: a build that resolves from it needs no other repository for the SDK itself.
-
Or install the jar copied with the release into the local repository, then build offline (
-o).mvn install:install-file -Dfile=enterprise-ai-java-1.0.2.jar -DpomFile=enterprise-ai-java-1.0.2.pommvn -o package -
Or build from this repository.
cd sdks/java && mvn -q install
Quickstart
This example builds a client, reads the caller identity and sends one chat completion through the gateway.
EnterpriseAIClient quick = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.build();
JsonNode identity = quick.meta().me();
ChatResponse reply = quick.gateway().chatCompletions(ChatRequest.builder("gpt-4o-mini").user("Bonjour").build());
System.out.println(identity.path("id").asText() + " " + reply.content());
What happens: build() checks the base URL (https required except on localhost), every call sends the key as
X-API-Key and a W3C traceparent, and the completion is posted with an Idempotency-Key. Expected result: the
caller id, then the text of the answer.
Authentication
The server resolves the tenant from the credential. The SDK reads no environment variable: pass the values to the
builder, for example with System.getenv.
| Method | Builder method | Header sent |
|---|---|---|
Tenant API key eai_<prefix>_<secret> | apiKey(String) | X-API-Key |
| OIDC access token | token(String) | Authorization: Bearer <token> |
Development mode (server started with EAI_AUTH_DEV=1), only without credential | tenant(String) | X-Dev-Tenant |
| Workload identity, mTLS | httpClient(HttpClient) with an SSLContext holding the client certificate | TLS client certificate |
EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.tenant("acme")
.project("invoices")
.locale(Locale.ENGLISH)
.build();
Client configuration
EnterpriseAIClient.builder() returns an EnterpriseAIClient.Builder:
| Builder method | Type | Default | Description |
|---|---|---|---|
baseUrl | String | none, required | Control Plane URL without /v1; https except for localhost, 127.0.0.1, ::1. |
apiKey | String | none | Tenant API key, sent as X-API-Key. |
token | String | none | OIDC access token, sent as Authorization: Bearer. |
tenant | String | none | Tenant slug; sent as X-Dev-Tenant only without credential. |
project | String | none | Sent as X-Project. |
environment | String | none | dev, test, staging or prod, sent as X-Environment. |
locale | Locale | Locale.ENGLISH | Locale.ENGLISH or Locale.FRENCH; sent as Accept-Language. |
retries | int | 2 | Retries after the first attempt. |
retryBaseDelay | Duration | 250 ms | First backoff delay, doubled at each attempt. |
timeout | Duration | 60 s | Timeout of one request. |
connectTimeout | Duration | 10 s | Connection timeout. |
caBundle | java.nio.file.Path | JDK trust store | PEM file of extra trusted CAs; verification stays on. |
dangerouslyDisableTlsVerificationForLocalhost | none | off | Disables certificate checks; build() refuses it unless the host is local. |
httpClient | java.net.http.HttpClient | built by the SDK | Custom client (proxy, executor); it must not follow redirects. |
traceparent | Supplier<String> | random per request | W3C traceparent of the current span. |
header | String, String | none | Extra header sent on every request. |
headerSupplier | Supplier<Map<String, String>> | none | Headers computed per request (they override project and environment). |
Per call options: RequestOptions.create().idempotencyKey(k).traceparent(tp).header(name, value), accepted by the
methods that have an overload with RequestOptions.
EnterpriseAIClient tuned = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.token(System.getenv("EAI_TOKEN"))
.environment("prod")
.retries(4)
.retryBaseDelay(Duration.ofMillis(500))
.timeout(Duration.ofSeconds(30))
.connectTimeout(Duration.ofSeconds(5))
.header("X-Client", "billing")
.build();
JsonNode created = tuned.agents().create(Map.of("kind", "Agent"), RequestOptions.create().idempotencyKey("agent-ap-1"));
Builder.toString() masks the API key and the token.
Requests and responses
Bodies are any Jackson serializable value (Map, record, POJO); results are JsonNode (read with path(...)), or
typed models for the gateway: ChatResponse (content(), usage(), decisionId(), costEstimated(),
costMeasured()) and ChatChunk (delta()). client.transport().exchange(...) returns the status and the headers
of any call.
ChatResponse r = client.gateway().chatCompletions(ChatRequest.builder("gpt-4o-mini")
.user("Bonjour")
.effort(Effort.LOW).agent("agt_ap", null, null, null)
.build());
System.out.println(r.costMeasured() + " " + r.decisionId());
try (ChatStream s = client.gateway().chatCompletionsStream(req)) { s.stream().forEach(c -> System.out.print(c.delta())); }
Errors
Every error answer uses the envelope {"error": {"code", "message", "message_i18n", "details", "request_id"}}.
The SDK throws an unchecked EaiException (or a subclass) with status() (0 for a network error), code(),
getMessage() (in the client locale), details() (secret looking keys masked), requestId() and
retryAfterSeconds(). Without an envelope the code comes from the status: 400 and 422 validation_error, 401
unauthenticated, 403 forbidden, 404 not_found, 409 conflict, 429 rate_limited, 5xx server_error.
| Exception | Codes | Retried by the SDK |
|---|---|---|
EaiException | any other code (validation_error, conflict, rate_limited, server_error, network_error...) | rate_limited (429), 408, 502, 503, 504 and network_error yes, then thrown |
AuthException | unauthenticated, forbidden, or HTTP 401 and 403 | no |
PolicyDeniedException | policy_denied | no |
QuotaExceededException | quota_exceeded, budget_exceeded | yes when the status is 429, then thrown |
EffortNotSupportedException | effort_not_supported | no |
ContextOverflowException | context_overflow | no |
ApprovalRequiredException | approval_required | no |
NotFoundException | not_found or HTTP 404 | no |
EgressDeniedException | egress_denied | no |
PromptInjectionBlockedException | prompt_injection_blocked | no |
PayloadTooLargeException | payload_too_large | no |
try { client.gateway().chatCompletions(req); }
catch (QuotaExceededException e) { log.warn("{} {} {}", e.code(), e.getMessage(), e.requestId()); }
client.runs().list(Map.of("limit", 50)).autoPaging().forEach(r -> System.out.println(r.path("id").asText()));
Retries and timeouts
An I/O error and HTTP 408, 429, 502, 503 and 504 are retried up to retries times (default 2), for every method:
a POST keeps its Idempotency-Key across retries, so the server deduplicates it. The delay starts at
retryBaseDelay (250 ms), doubles at each attempt up to 20 s and adds up to 20 % of jitter; a Retry-After header
wins, capped at 60 s. timeout (60 s) bounds each request and connectTimeout (10 s) each connection. An
interrupted thread stops at once with a network_error.
Pagination
List methods return a Page<JsonNode>: items(), nextCursor(), hasNext(), next() (the next page),
autoPaging() (an Iterable over every item across pages) and stream() (a Stream over every item).
Page<JsonNode> first = client.agents().list(Map.of("limit", 100));
System.out.println(first.items().size() + " " + first.hasNext());
long active = first.stream().filter(a -> "active".equals(a.path("status").asText())).count();
Idempotency and long operations
Every POST sends an Idempotency-Key: the RequestOptions.idempotencyKey(...) value, else a generated UUID kept
across the retries of the call. A long creation answers with the Operation-Id and Operation-Location headers;
client.operations().get(id) reads the operation, its tracked resource and its live state.
JsonNode run = client.runs().start("agt_ap", Map.of("task", Map.of("invoice_media_id", "med_01HZX3K9")), RequestOptions.create().idempotencyKey("inv-2291"));
JsonNode done = client.runs().getRaw(run.path("id").asText());
JsonNode operation = client.operations().get("op_1");
System.out.println(operation.path("status").asText());
Async and concurrency
The client is thread safe: share one instance between threads. Calls are blocking; run them on an executor (for
example virtual threads on Java 21) for concurrency. SseStream.publisher(executor) exposes a stream as a
java.util.concurrent.Flow.Publisher, and transport().websocket(...) returns a CompletableFuture<WebSocket>.
var pool = java.util.concurrent.Executors.newFixedThreadPool(4);
var meta = java.util.concurrent.CompletableFuture.supplyAsync(() -> client.meta().get(), pool);
var models = java.util.concurrent.CompletableFuture.supplyAsync(() -> client.models().list(), pool);
System.out.println(meta.join().path("version").asText() + " " + models.join().items().size());
pool.shutdown();
Streaming and realtime
gateway().chatCompletionsStream(req) returns a ChatStream (an Iterator<ChatChunk> and AutoCloseable with
stream()); close it with try-with-resources. Voice live sessions use client.realtime() and the WebSocket of the
transport: see the Realtime guide.
Logging and debugging
The SDK logs through SLF4J: the logger io.enterpriseai.Transport writes one DEBUG line per retry (method, path,
status or I/O error). Bind SLF4J to your logging backend (Logback, Log4j 2) and set the level of that logger.
Credentials are never logged.
<logger name="io.enterpriseai.Transport" level="DEBUG"/>
Give the requestId() of an exception to the platform team: it is the key of the audit event and of the trace.
Testing
Point the client at a local HTTP server: the real transport runs (headers, retries, exceptions). The JDK server
com.sun.net.httpserver.HttpServer needs no dependency; the SDK tests use OkHttp MockWebServer.
com.sun.net.httpserver.HttpServer server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/v1/agents", exchange -> {
byte[] body = "{\"items\":[{\"id\":\"agt_1\"}],\"next_cursor\":null}".getBytes(java.nio.charset.StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
EnterpriseAIClient testClient = EnterpriseAIClient.builder()
.baseUrl("http://127.0.0.1:" + server.getAddress().getPort())
.apiKey("eai_test_key")
.retries(0)
.build();
System.out.println(testClient.agents().list().items().size());
server.stop(0);
Versioning and compatibility
- The artifacts follow semantic versioning:
1.xkeeps every public class and method; a removal waits for2.0. - They target API
v1and the contractsv1;JsonNoderesults keep the fields a newer server adds. - Pin the version in the build and read the release notes before an upgrade.
Resource guides
One subsection per resource group, with one example per common call. Every method is in the generated reference.
Tools and connectors
tools(), mcp(), mcpRuntime(), skills(), plugins(), connectors(), credentials(), webhooks(),
channels(), navinLibrary(), fileSecurity().
client.tools().invoke("erp.lookup", Map.of("arguments", Map.of("invoice", "INV-2291")));
client.connectors().test("sharepoint");
Multimodal
media(), multimodal(), realtime().
JsonNode m = client.multimodal().vision(Map.of("model", "vision-default", "media_id", "med_IMG1", "prompt", "Total ?"));
client.multimodal().stt(Map.of("media_id", "med_AUD1", "language", "fr"));
Quality and audit
evaluations(), redteam(), audit(), telemetry().
JsonNode g = client.evaluations().checkGate(Map.of("target", Map.of("type", "agent", "ref", "agt_ap", "version", "1.4.0"), "suite_ids", List.of("evs_12")));
boolean ok = client.audit().verify().path("valid").asBoolean();
FinOps and quotas
finops(), quotas(). A Reservation is AutoCloseable: close() releases a reservation that was not committed.
JsonNode e = client.finops().estimate(Map.of("provider", "openai", "usage_type", "llm", "model_id", "gpt-4o-mini", "units", Map.of("input_tokens", 12000, "output_tokens", 2000)));
try (Reservation r = client.quotas().reserve(Map.of("cost", "0.50", "scope", Map.of("agent", "agt_ap")))) {
ChatResponse out = client.gateway().chatCompletions(req);
r.commit(Map.of("cost", out.costMeasured()));
} // close() releases an uncommitted reservation
Governance, SOC, storage, marketplace and evidence
license(), telemetry(), identity(), context(), delegations(), breakGlass(), soc(), storage(),
marketplace(), models(), policies(), evidence(), realtime().
JsonNode lic = client.license().get(); // GET /v1/license
JsonNode slo = client.telemetry().slo(Map.of("window_minutes", 60)); // GET /v1/telemetry/slo
JsonNode me = client.identity().whoami(); // GET /v1/identity/whoami
JsonNode ctx = client.context().issue(Map.of("purpose", "support"));
client.delegations().restrict("dlg_1", Map.of("scopes", List.of("read")));
client.breakGlass().request(Map.of("reason", "incident INC-42", "ttl_seconds", 900));
client.soc().createOrder(Map.of("incident_id", "inc_1", "action", "ISOLATE_NETWORK"));
client.soc().webhookSigned("falco", payload, secret, "acme"); // HMAC X-EAI-Signature
client.storage().presign("stp_1", Map.of("key", "exports/a.json", "scope", "read"));
client.marketplace().preflight(Map.of("package", "crm-agent", "version", "1.2.0"));
client.models().startRollout("mdl_1", "3", Map.of("percent", 10));
byte[] tgz = client.policies().downloadBundle("current"); // signed OPA bundle
client.evidence().verify();
client.realtime().turns("rts_1");
Webhooks, heartbeats and usage
webhooks(), heartbeats(), quotas(); TokenUnits.of(...) builds the token units of a Usage Event.
Map<String, String> h = WebhooksClient.signedHeaders(raw, secret, "acme", "evt-42", Instant.now().getEpochSecond());
client.webhooks().deliverSigned("sap", raw, secret, "acme");
Map<String, Object> units = TokenUnits.of(1200, 300, 800, 120);
if (HeartbeatsClient.shouldStop(client.heartbeats().beat(runId, Map.of("progress", 0.5)))) { /* checkpoint, exit */ }
try (Reservation r = client.quotas().reserve(Map.of("cost", "0.50", "tokens", 4000))) {
r.commit(Map.of("cost", "0.12", "tokens", 1800)); // r.id() is the "id" field
}
Architecture interfaces
SdkInterfaces.of(client) groups the named interfaces of the architecture. A refusal is thrown as its typed
exception (relation_required, approval_closed, storage_object_protected), and an answer that does not confirm
the result is thrown, never returned as a success.
SdkInterfaces api = SdkInterfaces.of(client);
try {
JsonNode assumed = api.identity().assumeService("si_billing", "invoice sync", List.of("erp"), 600, null);
String serviceToken = assumed.path("token").asText();
} catch (EaiException e) {
if ("relation_required".equals(e.code())) { /* write the can_assume relation first */ }
}
client.audit().record(Map.of("action", "app.invoice.validated", "resource", Map.of("type", "invoice", "id", "inv-42"), "outcome", "success"));
api.approval().cancel("apr_1", "request withdrawn");
JsonNode objects = client.storage().listObjects("stp_1", Map.of("prefix", "exports/", "limit", 100));
api.storage().delete("stp_1", "exports/old.json", null);
Platform Manager space (platform_admin)
platformManager() reaches the Platform Manager space of the console, relayed by the Control Plane on behalf of the
caller.
JsonNode summary = client.platformManager().summary(); // GET /v1/platform-manager/summary
JsonNode services = client.platformManager().services("c1"); // five state fields and administrative state
JsonNode op = client.platformManager().requestOperation("c1", "opa", Map.of("action", "rolling_restart", "batch_size", 1));
JsonNode restore = client.platformManager().planRestore(Map.of("artifact_id", "b1", "environment", "isolated")); // ADR-0162
JsonNode exercises = client.platformManager().drExercises(); // observed RPO and RTO
Spring Boot
The Spring Boot 3 starter enterprise-ai-spring creates an EnterpriseAIClient bean from the enterprise-ai.* properties
(base-url, api-key, token, tenant, project, environment, locale default en, retries default 2,
timeout default 60 s, tool-owner, interceptor default true) and, when enterprise-ai.manager.base-url is
set, a ManagerClient bean from enterprise-ai.manager.* (base-url, token, token-file, ca-bundle, on-behalf-of, locale, retries,
timeout default 30 s).
enterprise-ai:
base-url: <control-plane-url>
api-key: ${EAI_API_KEY}
tenant: acme
locale: fr
Navin Manager client
io.enterpriseai.manager.ManagerClient calls the Navin Manager API directly with a manager token
nmt_<12 hex>_<secret>; GET, PUT and DELETE are retried, POST and PATCH never. The full guide is
Navin Manager SDK.
io.enterpriseai.manager.ManagerClient nm = io.enterpriseai.manager.ManagerClient.builder()
.baseUrl("<manager-url>")
.token(System.getenv("NM_MANAGER_TOKEN"))
.onBehalfOf("alice")
.build();
JsonNode action = nm.services().action("c1", "kafka", Map.of("action", "restart"));
Reference
- Generated Java reference: every class, method, parameter and field.
- API v1 of the Control Plane, Contracts v1, Realtime guide.
- Other SDKs.