Skip to main content

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​

ItemSupported
Java17 or later (maven.compiler.release 17 in sdks/java/pom.xml)
Operating systemsEvery platform with a Java 17 runtime (Linux, macOS, Windows)
Dependenciesjackson-databind, jackson-datatype-jsr310, json-schema-validator (networknt), slf4j-api
Spring Boot starterenterprise-ai-spring, built against the spring-boot-dependencies BOM of the parent POM
Control Plane APIv1

Installation​

  1. Download the Maven repository from Harbor. The SDK packages are hosted in the sdk project of the Navinspire Harbor registry (artifact sdk/java, tag = SDK version). The Navinspire registry is registry.navinspire.ai; log in with a robot account that can pull from the sdk project (given by the platform team). If your organization mirrors the sdk project in its own Harbor, use the address of that mirror instead.

    oras login registry.navinspire.ai
    oras pull registry.navinspire.ai/sdk/java:1.0.2 -o sdk-java

    Expected result: the sdk-java folder holds the Maven repository of the SDK (io/enterpriseai/...).

  2. 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 (or gradle dependencies) lists io.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.

  1. 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.pom
    mvn -o package
  2. 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.

MethodBuilder methodHeader sent
Tenant API key eai_<prefix>_<secret>apiKey(String)X-API-Key
OIDC access tokentoken(String)Authorization: Bearer <token>
Development mode (server started with EAI_AUTH_DEV=1), only without credentialtenant(String)X-Dev-Tenant
Workload identity, mTLShttpClient(HttpClient) with an SSLContext holding the client certificateTLS 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 methodTypeDefaultDescription
baseUrlStringnone, requiredControl Plane URL without /v1; https except for localhost, 127.0.0.1, ::1.
apiKeyStringnoneTenant API key, sent as X-API-Key.
tokenStringnoneOIDC access token, sent as Authorization: Bearer.
tenantStringnoneTenant slug; sent as X-Dev-Tenant only without credential.
projectStringnoneSent as X-Project.
environmentStringnonedev, test, staging or prod, sent as X-Environment.
localeLocaleLocale.ENGLISHLocale.ENGLISH or Locale.FRENCH; sent as Accept-Language.
retriesint2Retries after the first attempt.
retryBaseDelayDuration250 msFirst backoff delay, doubled at each attempt.
timeoutDuration60 sTimeout of one request.
connectTimeoutDuration10 sConnection timeout.
caBundlejava.nio.file.PathJDK trust storePEM file of extra trusted CAs; verification stays on.
dangerouslyDisableTlsVerificationForLocalhostnoneoffDisables certificate checks; build() refuses it unless the host is local.
httpClientjava.net.http.HttpClientbuilt by the SDKCustom client (proxy, executor); it must not follow redirects.
traceparentSupplier<String>random per requestW3C traceparent of the current span.
headerString, StringnoneExtra header sent on every request.
headerSupplierSupplier<Map<String, String>>noneHeaders 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.

ExceptionCodesRetried by the SDK
EaiExceptionany other code (validation_error, conflict, rate_limited, server_error, network_error...)rate_limited (429), 408, 502, 503, 504 and network_error yes, then thrown
AuthExceptionunauthenticated, forbidden, or HTTP 401 and 403no
PolicyDeniedExceptionpolicy_deniedno
QuotaExceededExceptionquota_exceeded, budget_exceededyes when the status is 429, then thrown
EffortNotSupportedExceptioneffort_not_supportedno
ContextOverflowExceptioncontext_overflowno
ApprovalRequiredExceptionapproval_requiredno
NotFoundExceptionnot_found or HTTP 404no
EgressDeniedExceptionegress_deniedno
PromptInjectionBlockedExceptionprompt_injection_blockedno
PayloadTooLargeExceptionpayload_too_largeno
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.x keeps every public class and method; a removal waits for 2.0.
  • They target API v1 and the contracts v1; JsonNode results 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

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​