TypeScript SDK
The official TypeScript / JavaScript SDK for the Zilfu API — install, configure, and call every endpoint with full type safety.
Why an SDK?
The REST API is the source of truth, but if you're building in TypeScript or
JavaScript you shouldn't need to hand-roll fetches, hand-write types, or
hand-parse error responses. @zilfu/sdk is the official client. It is:
- Generated from the OpenAPI spec, so request and response types stay in lockstep with the live API.
- Promise-based, with a single
createZilfuClient(...)factory and one namespace per resource. - Strict on errors — non-2xx responses throw a typed
ZilfuApiError, so you cantry/catchinstead of branching onresult.error. - Multi-instance safe — every call to the factory returns an independent client, so you can talk to multiple workspaces or environments from the same process without them stepping on each other.
It works in Node 18.17+ and any runtime with global fetch (Bun, Deno, Vercel
Edge, Cloudflare Workers).
Install
pnpm add @zilfu/sdk
# or
npm install @zilfu/sdk
# or
yarn add @zilfu/sdkAuthenticate
You need a personal access token. If you don't have one, follow the Quickstart — it walks through creating one from Settings → API Tokens.
Treat your token like a password. Load it from an environment variable or a secrets manager — never commit it to source control or paste it into client-side code.
Configure
import { createZilfuClient } from "@zilfu/sdk";
const api = createZilfuClient({
baseUrl: "https://zilfu.app/api",
token: process.env.ZILFU_TOKEN!,
});createZilfuClient accepts:
| Option | Type | Required | Notes |
|---|---|---|---|
token | string | () => string | Promise<string> | yes | Bearer token. The function form is resolved on every request — useful for refresh flows. |
baseUrl | string | no | Defaults to https://zilfu.app/api. Override for staging or local Herd. |
fetch | typeof fetch | no | Custom fetch — e.g. undici.fetch, a wrapping fetch for retries, or a serverless polyfill. |
headers | Record<string, string> | no | Default headers merged into every request. |
Multiple clients can coexist:
const prod = createZilfuClient({ baseUrl: "https://zilfu.app/api", token: PROD_TOKEN });
const staging = createZilfuClient({ baseUrl: "https://staging.zilfu.app/api", token: STAGING_TOKEN });
await prod.spaces.list();
await staging.spaces.list();First Request
const { data } = await api.spaces.list();
console.log(data); // { data: [{ id: 1, name: "..." }, ...] }Most endpoints are scoped to a space:
const space = data!.data[0]!;
const posts = await api.posts.list({ path: { space: space.id } });Errors
Any non-2xx response throws a typed ZilfuApiError:
import { ZilfuApiError } from "@zilfu/sdk";
try {
await api.posts.create({
path: { space: "1" },
body: { /* invalid */ },
});
} catch (e) {
if (e instanceof ZilfuApiError) {
console.log(e.status); // e.g. 422
console.log(e.message); // human-readable summary
console.log(e.errors); // Laravel-style { field: ["msg", ...] } map, when present
console.log(e.body); // raw response body
}
}Network failures (DNS, TLS, abort, timeout) surface as ZilfuApiError with
status: 0 and the underlying error in body.
See the Rate Limits page for handling 429 responses
specifically — the SDK respects Retry-After if you read it from e.body,
but it does not retry automatically.
Custom fetch
For retries, logging, or edge runtimes:
import { fetch as undiciFetch } from "undici";
const api = createZilfuClient({
token: process.env.ZILFU_TOKEN!,
fetch: undiciFetch,
});Types
Every request and response type is exported from the package:
import type {
PostResource,
SpaceResource,
AccountResource,
WebhookResource,
StorePostRequest,
} from "@zilfu/sdk";Methods
The SDK exposes one namespace per resource. Every method takes a single
options object (path, query, body as needed) and returns
Promise<{ data, response }>. On non-2xx, it throws.
health
| Method | Endpoint |
|---|---|
api.health.check() | GET /health |
spaces
| Method | Endpoint |
|---|---|
api.spaces.list() | GET /spaces |
api.spaces.get({ path }) | GET /spaces/{space} |
api.spaces.create({ body }) | POST /spaces |
api.spaces.update({ path, body }) | PUT /spaces/{space} |
api.spaces.delete({ path }) | DELETE /spaces/{space} |
slots
| Method | Endpoint |
|---|---|
api.slots.list({ path }) | GET /spaces/{space}/slots |
api.slots.create({ path, body }) | POST /spaces/{space}/slots |
api.slots.delete({ path }) | DELETE /spaces/{space}/slots/{slot} |
queue
| Method | Endpoint |
|---|---|
api.queue.list({ path }) | GET /spaces/{space}/queue |
accounts
| Method | Endpoint |
|---|---|
api.accounts.list({ path }) | GET /spaces/{space}/accounts |
api.accounts.activate({ path }) | PATCH /spaces/{space}/accounts/{account}/activate |
api.accounts.boards({ path }) | GET /spaces/{space}/accounts/{account}/boards |
api.accounts.delete({ path }) | DELETE /spaces/{space}/accounts/{account} |
api.accounts.deleteMany({ path }) | DELETE /spaces/{space}/accounts |
posts
| Method | Endpoint |
|---|---|
api.posts.list({ path }) | GET /spaces/{space}/posts |
api.posts.get({ path }) | GET /spaces/{space}/posts/{post} |
api.posts.create({ path, body }) | POST /spaces/{space}/posts |
api.posts.update({ path, body }) | PUT /spaces/{space}/posts/{post} |
api.posts.delete({ path }) | DELETE /spaces/{space}/posts/{post} |
clusters
A cluster is a multi-account post group. Update one cluster atomically with:
| Method | Endpoint |
|---|---|
api.clusters.update({ path, body }) | PUT /spaces/{space}/clusters/{cluster_id} |
media
| Method | Endpoint |
|---|---|
api.media.create({ body }) | POST /media |
api.media.delete({ path }) | DELETE /media/{media} |
// FormData upload
const fd = new FormData();
fd.append("file", file);
const { data } = await api.media.create({ body: { file } });apiTokens
| Method | Endpoint |
|---|---|
api.apiTokens.create({ body }) | POST /api-tokens |
api.apiTokens.delete({ path }) | DELETE /api-tokens/{tokenId} |
webhooks
| Method | Endpoint |
|---|---|
api.webhooks.list({ path }) | GET /spaces/{space}/webhooks |
api.webhooks.create({ path, body }) | POST /spaces/{space}/webhooks |
api.webhooks.update({ path, body }) | PUT /spaces/{space}/webhooks/{webhook} |
api.webhooks.delete({ path }) | DELETE /spaces/{space}/webhooks/{webhook} |
subscription
| Method | Endpoint |
|---|---|
api.subscription.get() | GET /subscription |
bio and bio.blocks
The link-in-bio page lives under bio; its blocks live under
bio.blocks.
| Method | Endpoint |
|---|---|
api.bio.get({ path }) | GET /spaces/{space}/bio |
api.bio.create({ path, body }) | POST /spaces/{space}/bio |
api.bio.update({ path, body }) | PUT /spaces/{space}/bio |
api.bio.uploadAvatar({ path, body }) | POST /spaces/{space}/bio/avatar |
api.bio.blocks.list({ path }) | GET /spaces/{space}/bio/blocks |
api.bio.blocks.create({ path, body }) | POST /spaces/{space}/bio/blocks |
api.bio.blocks.update({ path, body }) | PUT /spaces/{space}/bio/blocks/{block} |
api.bio.blocks.reorder({ path, body }) | POST /spaces/{space}/bio/blocks/{block}/reorder |
api.bio.blocks.delete({ path }) | DELETE /spaces/{space}/bio/blocks/{block} |
API Reference
The SDK is regenerated from the live OpenAPI spec on every change. See the API Reference for the full request/response schemas and try-it-now playgrounds.