Build apps
SDK (TypeScript)
How to use @hane/sdk: creating a client, authentication, common calls, errors, Idempotency-Key, If-Match, and pagination.
This page explains how to use the TypeScript SDK @hane/sdk. The SDK consists of types generated from the public API’s OpenAPI document and a thin client on top of openapi-fetch. It runs in both browsers and Node.js.
What the SDK exports
Section titled “What the SDK exports”| Name | What it is |
|---|---|
createHaneClient(options) |
Creates a client that adds Authorization and Idempotency-Key |
unwrap(promise) |
Returns the response data, or throws HaneError on failure |
HaneError |
A failed response, with status, code, problem, and retryAfterSeconds |
ifMatch(version) |
Builds the If-Match value ("<version>") |
paths, components, Schemas |
Types generated from the OpenAPI document |
@hane/sdk/openapi.json |
The combined OpenAPI document of the public API |
There are no per-endpoint functions. You call client.GET("/v1/...") with the path. Paths, parameters, bodies, and responses are all typed.
Create a client
Section titled “Create a client”import { createHaneClient, unwrap } from "@hane/sdk";
const hane = createHaneClient({ baseUrl: process.env.HANE_API!, // API key or access token. Called on every request, so you can swap it when it expires token: async () => session.accessToken, // headers added to every request (optional) headers: { "x-hane-client": "my-app" },});token() is called on every request. After you exchange a new access token, the same client can return the new value. Pass fetch to replace how requests are sent.
Common calls
Section titled “Common calls”// list (by type, filtered with CEL)const page = await unwrap( hane.GET("/v1/data/records", { params: { query: { type: "LeaseContract", where: 'record.status == "active"', limit: 100 } }, }),);
// one record with its relations and historyconst { entity, relations } = await unwrap(hane.GET("/v1/data/records/{id}", { params: { path: { id } } }));
// createconst created = await unwrap( hane.POST("/v1/data/records", { body: { type: "Vehicle", values: { registration_no: { value: "TEST 300 A 10-01" } } } }),);
// open Tasks for me or my groupsconst tasks = await unwrap(hane.GET("/v1/approvals/tasks", { params: { query: { mine: true, status: "open" } } }));Handle errors
Section titled “Handle errors”Failed responses are RFC 9457 Problem Details. unwrap throws HaneError.
import { HaneError } from "@hane/sdk";
try { await unwrap(hane.POST("/v1/data/records", { body }));} catch (e) { if (e instanceof HaneError) { console.error(e.status, e.code, e.message); for (const x of e.problem?.errors ?? []) console.error(x.path, x.message); // per-field errors if (e.code === "RATE_LIMITED") await sleep((e.retryAfterSeconds ?? 1) * 1000); }}| code | HTTP | Meaning |
|---|---|---|
VALIDATION_FAILED |
400 | The body or parameters don’t match the contract. Per-field errors are in problem.errors |
UNAUTHENTICATED |
401 | Missing or invalid credentials |
POLICY_EPOCH_STALE |
401 | An old token issued before permissions changed. Exchange again |
FORBIDDEN |
403 | Missing scope or permission |
NOT_FOUND |
404 | Does not exist, or you cannot read it |
CONFLICT |
409 | Not possible in the current state |
IDEMPOTENCY_CONFLICT |
409 | A different request was sent with the same Idempotency-Key |
PRECONDITION_FAILED |
412 | The If-Match version differs from the current one |
PRECONDITION_REQUIRED |
428 | If-Match is missing |
RATE_LIMITED, BUDGET_EXCEEDED |
429 | Too many requests, or the budget is exceeded |
TENANT_SUSPENDED |
423 | The organization is under an emergency stop |
Errors with problem.retryable set to true can be retried later with the same request.
Idempotency-Key
Section titled “Idempotency-Key”POST requests with side effects require an Idempotency-Key. If the caller does not set one, the SDK adds a random key. A request with the same key returns the first stored response (kept for 7 days).
To avoid creating things twice when you retry after a network failure, choose the key yourself and reuse it on retries.
const key = crypto.randomUUID(); // reuse the same value on retriesawait unwrap( hane.POST("/v1/data/records", { params: { header: { "Idempotency-Key": key } }, body: { type: "Vehicle", values }, }),);Sending a different request with the same key fails with IDEMPOTENCY_CONFLICT. POST requests without side effects, such as token exchange or definition diffs, don’t need a key.
If-Match (optimistic locking)
Section titled “If-Match (optimistic locking)”Record PATCH requests, and claiming, releasing, and completing Tasks, need the version you read in If-Match. If someone changed it first, the request fails with PRECONDITION_FAILED; read it again and decide again.
import { ifMatch } from "@hane/sdk";
const { entity } = await unwrap(hane.GET("/v1/data/records/{id}", { params: { path: { id } } }));await unwrap( hane.PATCH("/v1/data/records/{id}", { params: { path: { id }, header: { "If-Match": ifMatch(entity.version) } }, body: { values: { odometer_km: { value: 41200 } }, reason: "Checked against the service record" }, }),);Pagination
Section titled “Pagination”Most lists accept limit and cursor and return next_cursor. Keep reading until next_cursor is null. When you filter with where, a page in the middle can be empty while more pages remain, so decide by next_cursor, not by item count.
import type { Schemas } from "@hane/sdk";
type Page = { items: Schemas["Record"][]; next_cursor: string | null };const all: Schemas["Record"][] = [];let cursor: string | null = null;do { const page: Page = await unwrap( hane.GET("/v1/data/records", { params: { query: { type: "Vehicle", limit: 500, ...(cursor ? { cursor } : {}) } } }), ); all.push(...page.items); cursor = page.next_cursor;} while (cursor);Parameters and limits for each list are in the API reference.