Skip to content
HaneDOCS日本語

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.

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.

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.

// 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 history
const { entity, relations } = await unwrap(hane.GET("/v1/data/records/{id}", { params: { path: { id } } }));
// create
const 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 groups
const tasks = await unwrap(hane.GET("/v1/approvals/tasks", { params: { query: { mine: true, status: "open" } } }));

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.

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 retries
await 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.

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" },
}),
);

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.