EnfinitOS Brand SDK — Java
com.enfinitos:sdk-brand — the read-only REST client a brand (advertiser) uses to query its own delivery proof, metering, and settlement records directly from the EnfinitOS platform, without going through the operator's reporting plane.
Mirrors @enfinitos/sdk-brand (TypeScript) and enfinitos_brand (Python) one-for-one. When one moves, the other moves.
Who should use it
A brand engineering team that wants to:
- pull a Merkle-rooted signed proof of every billable delivery, and verify it against the Auditor SDK;
- reconcile its own attribution numbers against the platform's metered usage before paying the invoice;
- iterate settlement invoices and lines for finance/AP;
- open a dispute (with signed counter-evidence) when the brand's own auditor disagrees with what was billed.
The SDK is scoped read-only to campaigns the calling brand owns. The single write operation — disputes.open — is bound to the brand's auditor key and is itself idempotent.
Authentication
Every request carries:
Authorization: Bearer <brand_api_key>— issued by the platform to a single brand tenant; rotateable; read-only on owned campaigns plus dispute-open;X-Enfinitos-Brand: <brand_id>— the brand's tenant id; allows the platform WAF to rate-limit per-tenant before auth decode.
The platform rejects any mismatch between the key's owner and the header.
Installation
This SDK is currently distributed as part of the EnfinitOS monorepo. Add as a Maven dependency:
<dependency>
<groupId>com.enfinitos</groupId>
<artifactId>sdk-brand</artifactId>
<version>0.0.1</version>
</dependency>
Requires Java 17+. Zero third-party runtime dependencies.
Getting started
import com.enfinitos.brand.EnfinitOSBrandClient;
import com.enfinitos.brand.CampaignsApi;
import com.enfinitos.brand.ProofApi;
var client = EnfinitOSBrandClient.builder()
.apiBaseUrl("https://api.enfinitos.com")
.brandId("brand_acme_co")
.apiKey(System.getenv("ENFINITOS_BRAND_API_KEY"))
.build();
// 1. List my campaigns.
var campaigns = client.campaigns().list(CampaignsApi.ListOptions.defaults());
for (var c : campaigns.items()) {
System.out.printf("%s [%s] billed=%s%n",
c.campaignId(), c.status(), c.totalBilledMinor());
}
// 2. Fetch a signed proof pack for one campaign.
var pack = client.proof().pack(campaigns.items().get(0).campaignId());
// 3. Verify against the Auditor SDK (separate artifact —
// com.enfinitos:sdk-auditor — performs Merkle + signature
// verification offline, with no network round-trip).
//
// var ok = new AuditorClient(...).verifyPack(pack);
// if (!ok.valid()) throw new IllegalStateException("proof failed");
Module reference
client.campaigns()
list(ListOptions)— cursor-paginated list of owned campaigns.get(campaignId)— single campaign.
ListOptions accepts (status, cursor, limit). null defaults are fine.
client.proof()
summary(campaignId)— cheap Merkle-root rollup (merkleRoot,recordCount, signer info).pack(campaignId)— full signedSignedProofPack; pass to the Auditor SDK for offline verification.chain(campaignId, ChainOptions)— cursor-paginated per-leaf records.
For verification use pack — the chain endpoint is for inspection and incremental reconciliation only.
client.metering()
summary(campaignId)— per-unit totals.breakdown(campaignId, MeteringUnit)— per-day, per-substrate rollup of a single billable unit.
client.settlement()
invoices(InvoicesOptions)— invoices issued to the brand; optional(from, to)window onissuedAt.invoice(invoiceId)— single invoice with lines.line(lineId)— single invoice line; carries theproofSliceRootthat pins it to the corresponding Merkle subtree.
client.disputes()
open(OpenInput)— open a dispute. The SDK auto-generates an idempotency key when one isn't supplied; cron re-runs are safe.list(ListOptions)— list the brand's disputes.get(disputeId)— single dispute.
The dispute body's free-form reason is informational; the operator's response is bound to the SignedEvidence only.
Error model
The SDK raises two exception families:
EnfinitOSApiException— the platform answered with a non-2xx status or a2xxenvelope carryingok: false. Carriescode(stable string identifier),httpStatus,correlationId, optionaldetails, and anisRetryable()helper.EnfinitOSTransportException— connection-level failure (DNS, timeout, TLS, refused).
Typical pattern:
try {
client.campaigns().get(id);
} catch (EnfinitOSApiException e) {
if (e.isRetryable()) {
// 408 / 429 / 5xx — defer to your scheduler's backoff.
} else if ("CAMPAIGN_NOT_FOUND".equals(e.code())) {
// tenant-bound 404 — the campaign either doesn't exist
// or isn't owned by this brand. The platform deliberately
// does NOT distinguish the two.
} else {
throw e;
}
}
The SDK does NOT retry by default. Brand-side systems sit downstream of the brand's own retry middleware (Sidekiq, JVM schedulers, Spring Retry, ...); doubling them up has caused duplicate-dispute filings in the past. Opt in by checking isRetryable() and re-issuing yourself.
Cross-reference
- Auditor SDK (
com.enfinitos:sdk-auditor) — offline verification of signed proof packs returned byproof.pack. Pure crypto; no network. - Operator Reporting — the platform's operator-facing report surface answers the same questions from the operator's point of view; brands deliberately do NOT consume it directly.
- REST contract —
apps/apiis the source of truth; the SDK pins shapes via record types and is regenerated when contracts move.
Wire conventions
Every request also includes:
X-Enfinitos-Sdk: brand-javaX-Enfinitos-Sdk-Version: 0.0.1X-Enfinitos-Contract: 1User-Agent: enfinitos-sdk-brand-java/0.0.1 [(tag)]Idempotency-Key: <uuid>onPOST /v1/brand/disputes(auto-generated by the SDK when the caller omits one).
The platform's response envelope is:
{ "ok": true, "data": { ... }, "contractVersion": 1 }
or
{ "ok": false, "error": { "code": "...", "message": "...", "correlationId": "..." } }
The SDK unwraps both and surfaces only data or the typed EnfinitOSApiException. The X-Contract-Version response header is captured on errors for drift-monitoring tooling.
Tests
mvn -q test
Tests use the injectable HttpTransport.Doer interface; no real HTTP server is required.