Client SDKs
What it is
trstctl ships supported client SDKs for Go, TypeScript, Python, and Java, plus blessed generator configs, so integrators do not have to hand-roll a client, retry logic, idempotency, pagination, or error mapping. The SDKs are generated/blessed against the served OpenAPI 3.1 contract and are pinned to it so they cannot silently drift from the API.
“Ships” currently means the source, generated contract, tests, and local build metadata are committed in this repository. It does not mean every library is published to a public package registry. The exact distribution boundary is:
| SDK | Verified way to use it today | Public package claim |
|---|---|---|
| Go | clone the repository and use a local replace to clients/sdk/go |
none; the vanity go-import endpoint is not served |
| TypeScript | npm install the checked-out clients/sdk/typescript directory |
none |
| Python | python -m pip install the checked-out clients/sdk/python directory |
none |
| Java | build/install the checked-out clients/sdk/java/pom.xml into the local Maven repository |
none |
The console uses only these verified local-source commands. A registry command can replace one of them only after CI installs that exact published artifact from a clean environment.
Code: clients/sdk/ (go/, typescript/, python/, java/, and the pinned openapi.json).
Regenerate: make sdk. Served contract + library.
Why it exists
The control plane already publishes its full OpenAPI 3.1 document (at
/api/v1/openapi.json) and a CLI at parity with the API. The SDKs add the next
layer every language team would otherwise re-implement:
- Auth — an
Authorization: Bearer <token>header on every call (and an optionalX-Tenant-IDhint for header/dev auth). - Idempotency — an
Idempotency-Keyon every mutation, auto-generated when you do not supply one and held stable across automatic retries, so a retried create is exactly-once on the server (a retry never applies the change twice). - problem+json (RFC 7807) — non-2xx responses parse into a typed error
(
*trstctl.Problemin Go,TrstctlProblemin TypeScript,ProblemErrorin Python,ProblemExceptionin Java) carryingstatus/title/detail/type/instanceand any extension members. - Retries with backoff —
429/502/503/504are retried with exponential backoff that honors aRetry-Afterheader when the server sends one. - Cursor pagination — list endpoints return
{ items, next_cursor }; the SDK exposes iterators that follownext_cursorso you never juggle cursors by hand.
How it works
Pinned to the served contract
The published spec lives at clients/sdk/openapi.json. A CI check fails the build
if that file drifts from the spec the running server actually serves, and a second
check pins that served spec to the live route table. So a backend field
add/rename/remove turns the build red until the SDKs are regenerated with
make sdk — the SDKs cannot desync from the API undetected.
Generation
# Re-pin clients/sdk/openapi.json to the served spec and regenerate every SDK.
make sdk
# CI guard: fail if the SDKs are out of sync with the served contract.
make sdk-check
# Build + test the Go, TypeScript, Python, and Java SDKs.
make sdk-test
Under the hood scripts/gen-sdk.sh copies the served golden to
clients/sdk/openapi.json, runs openapi-typescript for the TypeScript types,
runs the Python TypedDict generator, generates the Java OpenAPI schema index, and
(opt-in) oapi-codegen for the full Go model set.
Use it
Go
The Go SDK is its own module (trstctl.com/sdk/go) that imports nothing
outside the standard library, so adding it never drags the control plane's
dependency graph into your build.
trstctl.com/sdk/go is a vanity import path and no go-import endpoint is
served for it yet, so go get trstctl.com/sdk/go does not resolve on its own.
Read the Install section of clients/sdk/README.md first: it gives the exact
meta tag the path needs and the replace fallback that works today.
import (
"context"
"log"
trstctl "trstctl.com/sdk/go/trstctl"
)
func main() {
// Auth: the token is sent as Authorization: Bearer on every call.
client := trstctl.New("https://localhost:8443", "trst_...")
ctx := context.Background()
// Getting-started flow in one call: create owner -> create identity ->
// transition to "issued". Each step carries its own Idempotency-Key, so a
// retry never applies the change twice.
ident, err := client.IssueFirstCertificate(ctx, "payments")
if err != nil {
// problem+json errors surface as a typed *trstctl.Problem.
if prob, ok := trstctl.AsProblem(err); ok {
log.Fatalf("issuance failed: %d %s — %s", prob.HTTPStatus, prob.Title, prob.Detail)
}
log.Fatal(err)
}
log.Printf("issued identity %s (status %s)", ident.ID, ident.Status)
// Cursor pagination: the iterator follows next_cursor across pages.
it := client.Certificates(trstctl.CertificateListOptions{ListOptions: trstctl.ListOptions{Limit: 50}})
for it.Next(ctx) {
cert := it.Value()
log.Printf("certificate %s: %s", cert.ID, cert.Subject)
}
if err := it.Err(); err != nil {
log.Fatal(err)
}
}
Idempotency, retries (429/5xx with Retry-After), and the X-Tenant-ID
hint are configured on the client:
client := trstctl.New("https://localhost:8443", "trst_...",
trstctl.WithTenant("11111111-1111-1111-1111-111111111111"),
trstctl.WithRetry(trstctl.RetryPolicy{MaxAttempts: 4, BaseDelay: 200 * time.Millisecond, MaxDelay: 5 * time.Second}),
)
// Supply your own stable Idempotency-Key when you want a retry across process
// restarts to remain exactly-once:
owner, err := client.CreateOwnerKeyed(ctx, trstctl.OwnerRequest{Kind: "workload", Name: "payments"}, "my-stable-key")
The same dependency-free client has typed agentic/workload journeys rather than forcing an automation author to hand-roll HTTP:
PreviewBrokerAgentIdentity,IssueBrokerAgentIdentityKeyed,ListBrokerAgentIdentities, andGetBrokerAgentIdentitycover effect-free review, exact issuance retry, and durable broker history.PreviewAttestedSVIDandIssueAttestedSVIDKeyedcover direct attested X.509-SVID issuance.PreviewEphemeralCredential,IssueEphemeralCredentialKeyed, andApproveEphemeralCredentialKeyedpreserve the difference between a pending202approval and a completed201credential.
Proof and optional task-envelope inputs are caller-owned []byte. The SDK
base64-encodes them for the OpenAPI wire contract, wipes its own encoded buffers
after each attempt, keeps a supplied idempotency key stable across retries, and
refuses redirects for proof-bearing requests. Optional SPIFFEID fields remain
nil when the exact signed URI is unavailable; the SDK never promotes a friendly
subject into a signed identity. See the Go section of clients/sdk/README.md for
the copyable review-and-issue example and ambiguous-success error handling.
TypeScript
The TypeScript SDK's resource types are generated by openapi-typescript from
the pinned spec; a small dependency-free runtime adds the same auth, idempotency,
retry, problem+json, and pagination behavior. It requires a global fetch
(Node 18+, Deno, or a browser). The package is ready to publish as
@trstctl/sdk, includes public registry publish metadata, and carries a Node
test suite for bearer auth, optional tenant hints, core owner/identity/certificate
resources, cursor pagination, stable mutation Idempotency-Key retry behavior,
and typed problem+json errors.
import { TrstctlClient, isProblem } from "@trstctl/sdk";
const client = new TrstctlClient({
baseUrl: "https://localhost:8443",
token: "trst_...",
// tenant: "11111111-1111-1111-1111-111111111111", // optional X-Tenant-ID hint
});
try {
// create owner -> create identity -> transition to "issued", each with an
// auto-generated Idempotency-Key.
const ident = await client.issueFirstCertificate("payments");
console.log(`issued identity ${ident.id} (status ${ident.status})`);
} catch (err) {
if (isProblem(err)) {
console.error(`issuance failed: ${err.httpStatus} ${err.title} — ${err.detail}`);
} else {
throw err;
}
}
// Cursor pagination via an async generator that follows next_cursor.
for await (const cert of client.certificates({ limit: 50 })) {
console.log(`certificate ${cert.id}: ${cert.subject}`);
}
See clients/sdk/README.md and clients/sdk/typescript/README.md for the full
reference, including the exact npx openapi-typescript command and the
copy-paste helper snippet. make sdk-test runs both npm run typecheck and
npm test for the TypeScript package.
Python
The Python SDK package is trstctl-sdk. Its runtime is standard-library only,
and trstctl_sdk.types is generated from the pinned OpenAPI schemas. It is
covered by two gates: unit tests for auth/idempotency/problem handling, and a
served acceptance test that shells out to Python and performs bearer auth,
dynamic PKI issuance, and secret create/read/rotate/delete against the assembled
control-plane handler.
from trstctl_sdk import ProblemError, TrstctlClient
client = TrstctlClient.from_env()
try:
issued = client.issue_pki_secret(
"payments.service",
ttl_seconds=900,
idempotency_key="payments-pki-2026-06-25",
)
client.create_secret(
"apps/payments/api-token",
"initial-fixture-value",
idempotency_key="payments-secret-create",
)
current = client.get_secret("apps/payments/api-token")
except ProblemError as exc:
print(exc.http_status, exc.title, exc.detail)
raise
print(issued["serial"], current["version"])
Use TRSTCTL_SERVER, TRSTCTL_TOKEN, and TRSTCTL_TENANT with
TrstctlClient.from_env().
Java
The Java SDK package is com.trstctl:trstctl-sdk. Its runtime is JDK-only:
java.net.http for transport, a small in-package JSON codec, and generated
OpenApiSchemas drift metadata from the pinned OpenAPI schema names. It has the
same operational behavior as the other SDKs: bearer auth, optional X-Tenant-ID,
stable Idempotency-Key, ProblemException, Retry-After, and helpers for dynamic
PKI issuance plus secret create/read/rotate/delete.
import com.trstctl.sdk.PkiSecret;
import com.trstctl.sdk.Secret;
import com.trstctl.sdk.TrstctlClient;
TrstctlClient client = TrstctlClient.fromEnv();
PkiSecret issued = client.issuePkiSecret(
"payments.service",
900,
"payments-pki-2026-06-25"
);
Secret current = client.createSecret(
"apps/payments/api-token",
"initial-fixture-value",
"payments-secret-create"
);
System.out.println(issued.serial() + " " + current.version());
clients/sdk/java/scripts/run_tests.sh compiles the SDK and a JDK-only unit test.
CI runs the Java SDK gate in required mode, so the check fails instead of silently
skipping if the JDK disappears from the runner. The served-path acceptance test
compiles and runs a Java round-trip program against the assembled control plane when
a JDK is present.
Pitfalls & limits
- A backend contract change requires
make sdkregeneration — CI fails red until the pinned spec and SDKs are back in sync; do not hand-editclients/sdk/openapi.json. - The TypeScript runtime needs a global
fetch(Node 18+, Deno, or a browser); the Java SDK needs a JDK (java.net.http); the Go and Python runtimes are standard-library only. - The SDKs cover the served core contract; licensed EE-only routes are not in the pinned spec.
Reference
- Packages: Go
trstctl.com/sdk/go· TypeScript@trstctl/sdk· Pythontrstctl-sdk· Javacom.trstctl:trstctl-sdk. - Make targets:
make sdk(re-pin + regenerate),make sdk-check(CI drift guard),make sdk-test(build + test all four). - Env for
from_env()/fromEnv():TRSTCTL_SERVER,TRSTCTL_TOKEN,TRSTCTL_TENANT. - Shared behavior: bearer auth, optional
X-Tenant-ID, stableIdempotency-Keyon mutations, RFC 7807 typed errors,Retry-After-aware backoff on429/502/503/504, cursor iterators over{ items, next_cursor }.
See also
Platform & API (the contract these pin to) ·
Build on the API journey ·
clients/sdk/README.md and the per-language READMEs