Zilfu

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 can try/catch instead of branching on result.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/sdk

Authenticate

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:

OptionTypeRequiredNotes
tokenstring | () => string | Promise<string>yesBearer token. The function form is resolved on every request — useful for refresh flows.
baseUrlstringnoDefaults to https://zilfu.app/api. Override for staging or local Herd.
fetchtypeof fetchnoCustom fetch — e.g. undici.fetch, a wrapping fetch for retries, or a serverless polyfill.
headersRecord<string, string>noDefault 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

MethodEndpoint
api.health.check()GET /health

spaces

MethodEndpoint
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

MethodEndpoint
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

MethodEndpoint
api.queue.list({ path })GET /spaces/{space}/queue

accounts

MethodEndpoint
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

MethodEndpoint
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:

MethodEndpoint
api.clusters.update({ path, body })PUT /spaces/{space}/clusters/{cluster_id}

media

MethodEndpoint
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

MethodEndpoint
api.apiTokens.create({ body })POST /api-tokens
api.apiTokens.delete({ path })DELETE /api-tokens/{tokenId}

webhooks

MethodEndpoint
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

MethodEndpoint
api.subscription.get()GET /subscription

bio and bio.blocks

The link-in-bio page lives under bio; its blocks live under bio.blocks.

MethodEndpoint
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.

On this page