Skip to main content

Client & configuration

The client is the entry point: it owns a shared HTTP connection and token handling and exposes one accessor per service. It is safe to share — create one and reuse it for the lifetime of your application.

import ai.intellistream.datahub.sdk.client.DatahubClient;
import ai.intellistream.datahub.sdk.client.DatahubConfig;

// from the environment (and a .env file, if present)
DatahubClient client = DatahubClient.fromEnv();

// or explicitly
DatahubClient client = DatahubClient.create(DatahubConfig.builder()
.baseUrl("https://api.intellistream.ai")
.token(System.getenv("TOKEN"))
.build());

Services

ServiceJavaPythonRust
Resourcesclient.resources()client.resourcesapi.resources
Time-seriesclient.timeseries()client.timeseriesapi.time_series
Datasetsclient.datasets()client.datasetsapi.datasets
Eventsclient.events()client.eventsapi.events
Unitsclient.units()client.unitsapi.units
Filesclient.files()client.filesapi.files
Subscriptionsclient.subscriptions()client.subscriptionsapi.subscriptions

Authentication

All three clients read the same configuration: a base URL plus either a static bearer token or OAuth2 client-credentials (the SDK fetches and refreshes the token).

VariableMeaning
BASE_URLAPI base URL (required)
TOKENStatic bearer token
CLIENT_ID / CLIENT_SECRET / TOKEN_URIOAuth2 client-credentials (all three)
PROJECT_NAMEOptional project/tenant hint

fromEnv() / from_env() / create_api_service() read these from the environment, falling back to a .env file in the working directory (real environment variables win).

Provider-specific parameters

scope and audience are left out of the token request unless you set them. Keycloak wants neither; other providers refuse without them.

VariableJava builderPython kwargRust setterWhen you need it
SCOPE.scope(...)scope=set_scope(...)Entra ID requires api://<app-id-uri>/.default. Space-separate several.
AUDIENCE.audience(...)audience=set_audience(...)Auth0 requires it. Keycloak ignores it.

Exchanging an external token (jwt-bearer)

A token minted by one issuer is not accepted by an API that trusts another. To bridge them, the SDK can present an externally-issued JWT as an RFC 7523 assertion and exchange it for a token the API does accept. The common case is reaching a Keycloak-backed API with an Entra ID service principal:

1. client_credentials 2. jwt-bearer 3. Bearer
────────────────► ────────────────► ────────────────►
Entra token endpoint Keycloak token endpoint DataHub API
→ the assertion → the token you use

Setting an assertion source switches the request at TOKEN_URI from client-credentials to jwt-bearer. CLIENT_ID/CLIENT_SECRET/TOKEN_URI then describe the client performing the exchange, and the ASSERTION_* keys describe where the assertion comes from:

VariableJava builderPython kwargRust setterMeaning
ASSERTION.assertion(...)assertion=set_assertion(...)A ready-made JWT. Never refreshed — prefer the credentials below.
ASSERTION_CLIENT_ID / ASSERTION_CLIENT_SECRET / ASSERTION_TOKEN_URI.assertionCredentials(...)assertion_client_id= / assertion_client_secret= / assertion_token_url=set_assertion_credentials(...)Fetch the assertion with client credentials from another provider (all three).
ASSERTION_SCOPE.assertionScope(...)assertion_scope=set_assertion_scope(...)scope for the assertion request.
ASSERTION_AUDIENCE.assertionAudience(...)assertion_audience=set_assertion_audience(...)audience for the assertion request.
Python names the URL parameters *_url

The Python client already spells TOKEN_URI as token_url, so the assertion equivalent is assertion_token_url. The environment variables keep the _URI spelling in all three SDKs.

DatahubClient client = DatahubClient.create(DatahubConfig.builder()
.baseUrl("https://api.intellistream.ai")
// leg 2 — the confidential Keycloak client that performs the exchange
.clientCredentials("datahub-jwt-grant", keycloakSecret,
"https://keycloak.example.com/realms/datahub/protocol/openid-connect/token")
// leg 1 — the Entra app registration the assertion comes from
.assertionCredentials(entraAppId, entraSecret,
"https://login.microsoftonline.com/" + tenantId + "/oauth2/v2.0/token")
.assertionScope("api://" + entraAppId + "/.default")
.build());

Or entirely from the environment:

BASE_URL=https://api.intellistream.ai
CLIENT_ID=datahub-jwt-grant
CLIENT_SECRET=...
TOKEN_URI=https://keycloak.example.com/realms/datahub/protocol/openid-connect/token
ASSERTION_CLIENT_ID=<entra-application-id>
ASSERTION_CLIENT_SECRET=...
ASSERTION_TOKEN_URI=https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
ASSERTION_SCOPE=api://<entra-application-id>/.default

The exchanged token is cached and refreshed exactly like a client-credentials one. The assertion itself is never cached — providers commonly reject a replayed assertion, so every exchange starts from a fresh request.

Server-side setup is required

The identity provider must be configured to trust the external issuer, and the external identity must map to a real user on that side. For Keycloak that means an Identity Provider with JWT Authorization Grant enabled (Keycloak 26.5+), a client with the matching capability, and a linked user carrying the roles and tenant claim. See EntraID.md in the platform repository for the full walkthrough, including the audience and assertion-lifetime settings that trip up a first attempt.

From HashiCorp Vault (Java)

The Java client can also read the same keys from a Vault KV v2 secret, with a token or with AppRole:

DatahubConfig cfg = DatahubConfig.fromVault(vaultAddr, vaultToken, "datahub/sdk");
DatahubConfig cfg = DatahubConfig.fromVaultEnv("datahub/sdk"); // VAULT_ADDR + VAULT_TOKEN
DatahubConfig cfg = DatahubConfig.fromVaultAppRole(vaultAddr, roleId, secretId, "datahub/sdk");
DatahubConfig cfg = DatahubConfig.fromVaultAppRoleEnv("datahub/sdk"); // VAULT_ADDR + VAULT_ROLE_ID + VAULT_SECRET_ID

Durable ingest buffering

Optional and off by default. When enabled, datapoint and event ingestion that can't reach the API — or is rejected with an auth failure (HTTP 401/403, e.g. an expired or rotated token) — spools to disk and is flushed automatically on the next ingest call, so neither a transient outage nor a credential hiccup loses data or raises. The buffer is a segmented, compressed log (gzip in Java, zstd in Rust/Python) bounded on two axes, either of which may be left unset; an unset axis defaults to 72 hours / 5 GiB once buffering is on:

  • time — datapoints/events older than the window are dropped.
  • size — when the on-disk spool exceeds the cap, the oldest segment is dropped.

It is memory-safe: the spool is drained in segments, so even a multi-gigabyte buffer never loads into memory, and it is recovered from disk on the next start.

DatahubClient client = DatahubClient.create(DatahubConfig.builder()
.baseUrl("https://api.intellistream.ai")
.token(System.getenv("TOKEN"))
.enableBuffering() // 72 h / 5 GiB defaults
// .bufferRetention(Duration.ofMinutes(60)) // override the time window
// .bufferMaxBytes(2L * 1024 * 1024 * 1024) // override the size cap
// .bufferDirectory(Path.of("datahub-spool")) // default: .datahub-spool
.build());

IngestResult r = client.timeseries().ingest(byExternalId);
if (r.buffered() > 0) {
// server unreachable: r.buffered() datapoints are spooled, retried on the next call
}

fromEnv() instead reads BUFFER_RETENTION (an ISO-8601 duration, e.g. PT72H), BUFFER_MAX_BYTES and BUFFER_DIRECTORY — setting either bound turns buffering on.

Retries are idempotent

A flush re-sends buffered data, which is safe: datapoints are keyed by (series, timestamp) and events by id, so the backend collapses duplicates. The SDK stamps each event with a time-ordered UUID v7 before the first send, so a retried event keeps the same id (see Events).

Results & errors

Most calls return the entity (or a thin wrapper around a list of them); a non-2xx response surfaces as an exception/error carrying the HTTP status and the raw body.

Methods return DataWrapper<T>getItems() holds the results. Non-2xx throws DatahubApiException:

import ai.intellistream.datahub.models.IdCollection;
import ai.intellistream.datahub.sdk.http.DatahubApiException;

try {
DataWrapper<Resource> r = client.resources().byIds(List.of(IdCollection.createFromExternalId("pump_1")));
r.getItems().forEach(System.out::println);
} catch (DatahubApiException e) {
System.err.println(e.statusCode() + ": " + e.body());
}
Entity ids are JSON strings on the wire

64-bit ids are serialized as JSON strings so they survive JavaScript's 2⁵³ number limit. Each client reads them back into a native integer, so this only matters if you inspect raw responses.

Batch writes are all-or-nothing

Every call that takes a list is validated in full before anything is written, so one bad item in 500 creates nothing and the error names every offending item rather than the first. Retry the whole batch once you have fixed them.

Two responses are worth recognising by shape:

  • 400 with type: ".../errors/naming-policy" — one or more external ids broke the configured naming policy. Nothing was created; the violations array names each one and suggests a replacement.
  • A warnings array beside items on a 2xx — the write succeeded, and the ids in it are in a data steward's queue. The field is absent when empty, so existing code is unaffected.

Both shapes, and the rules behind them, are in External ids & naming.