Skip to content

TypeScript client (@dustid/apid-client)

@dustid/apid-client is the typed TypeScript client for the DUST API. It is the same client the DICE web application uses in production, and its request/response types are generated directly from the API’s OpenAPI spec, so it stays in lockstep with the server.

The package is not published to the public npm registry. It is distributed by DUST and available to integration customers on request (contact support@dustidentity.com). npm install @dustid/apid-client will not find it.

If you’d rather not take the dependency, you can get the same type safety by generating types from the OpenAPI spec — that is exactly how this client’s own types are produced — and call the API with plain fetch. The quickstart does it that way so it runs with nothing installed.

Check these before planning around the client:

RequirementDetail
RuntimeA global fetch: Node.js 18+, Bun, or Deno. You can inject your own implementation through the fetcher option (proxies, retries, test doubles).
TypeScript toolchainThe package’s entry points resolve to TypeScript source, not compiled JavaScript. Your bundler or runtime must transpile it — plain node dist/app.js against untranspiled source will not work.
Runtime dependenciesThe client is not dependency-free: it declares arktype (used to validate error payloads) plus DUST-internal helper packages, which come with the distribution. Budget for them in any vendoring, licence review, or air-gapped install.
Execution locationServer-side only — see the caution at the end of this page.
import { ApidClient } from "@dustid/apid-client";
const client = new ApidClient({
baseUrl: "https://apid.dustid.io",
bearerToken: token, // Authorization: Bearer <token>
organizationId: orgId, // sent as Dust-Ctx-Org-Id
teamId: teamId, // sent as Dust-Ctx-Team-Id
});

The full options type:

type ApidClientOptions = {
baseUrl: string;
bearerToken?: string;
organizationId?: string; // Dust-Ctx-Org-Id header
teamId?: string; // Dust-Ctx-Team-Id header
fetcher?: typeof globalThis.fetch; // custom fetch (proxies, testing)
defaultHeaders?: HeadersInit | (() => HeadersInit); // e.g. Dust-Ctx-Locale
logger?: Logger; // debug/error request logging
};

organizationId and teamId map onto the context headers; when teamId is omitted the server defaults to the organization root team. Use defaultHeaders for anything extra you want on every request — for example localized error messages:

const client = new ApidClient({
baseUrl,
bearerToken,
organizationId,
defaultHeaders: { "Dust-Ctx-Locale": "zh-CN" },
});

Clients are immutable; two helpers return a re-configured copy, which makes per-request or per-user scoping cheap:

const asOtherTeam = client.withContext({ teamId: otherTeamId });
const asFreshToken = client.withToken(newBearerToken);

The client groups endpoints into resources: client.me, client.threads, client.bundles, client.files, client.tags (Identifier operations — the /api/v1/tags/* endpoints), client.teams, client.sharing, client.templates, client.events, client.relations, client.threadLinks, client.assemblies, client.transfers, client.slices, client.imports, client.fabric, client.users, client.certificates, and client.certificateForms.

Method names mirror the reference. Two real examples with threads:

// GET /api/v1/threads — cursor-paginated list
const page = await client.threads.list({ pageSize: 50, q: "tire" });
for (const thread of page.threads) {
console.log(thread.threadId, thread.name);
}
if (page.next) {
const nextPage = await client.threads.list({ pageSize: 50, cursor: page.next });
}
// POST /api/v1/threads — create, unwrapped to the single created record
const created = await client.threads.createOne({
type: "single",
thread: { name: "Tire SZ3J-11-ZJ17" },
data: [{ name: "Serial Number", type: "text", value: { text: "SZ3J-11-ZJ17" } }],
});
// GET /api/v1/threads/{thread_id}
const record = await client.threads.get(created.threadId);
console.log(record.thread.name, record.events.length);

threads.create returns the raw batch response ({ created, uploadResponses }); threads.createOne is a convenience that returns created[0] and throws if the server created nothing.

All request and response types are exported from the package root (ThreadCreateRequest, ThreadQueryResponse, ThreadGetResponse, …), along with the raw generated paths / components / operations types from the spec.

Methods return the parsed JSON response on success and throw ApiError on any non-2xx status. ApiError carries the API’s standard error body:

import { ApiError } from "@dustid/apid-client";
try {
await client.threads.get(threadId);
} catch (error) {
if (error instanceof ApiError) {
// error.code stable error code, e.g. "NOT_FOUND", "UNAUTHORIZED"
// error.status HTTP status number
// error.message localized human-readable message
// error.detail optional extra context (validation issues, etc.)
// error.body the full { code, message, status, detail } payload
if (error.code === "UNAUTHORIZED") {
// token expired — re-exchange the API key and retry
}
} else {
throw error; // network failure or non-JSON response
}
}

Two edge behaviors worth knowing: 204/205 responses resolve to undefined, and a response that isn’t valid JSON throws a plain Error (not ApiError). If you pass a logger, every failed request is logged with its x-request-id for support correlation.

The API serves its OpenAPI 3 spec at https://apid.dustid.io/api/openapi.json. openapi-typescript turns it into a fully typed paths/components definition you can use with plain fetch or any spec-driven fetch wrapper:

Terminal window
npx openapi-typescript@7 https://apid.dustid.io/api/openapi.json -o <generated-types-file>
import type { paths } from "./dust-api";
type ThreadList =
paths["/api/v1/threads"]["get"]["responses"]["200"]["content"]["application/json"];

Remember to set the Authorization, Dust-Ctx-Org-Id, and Dust-Ctx-Team-Id headers yourself — see Request conventions. Regenerate the types whenever you pick up new API features; the spec at /api/openapi.json is always current for the server you fetched it from.