Skip to main content

TypeScript SDK

@kanera/sdk is a typed client for the Kanera public API. It has no runtime dependencies and is built on global fetch, so it runs unchanged on Node 18+, Bun, Deno, Cloudflare Workers, and browsers.

npm install @kanera/sdk
import { Kanera } from "@kanera/sdk";

const kanera = new Kanera({ apiKey: process.env.KANERA_API_KEY! });

const session = await kanera.session();
if (session.scope === "read") throw new Error("this credential cannot write");

for await (const card of kanera.work.iterateCards()) {
console.log(card.key, card.title);
}

await kanera.cards.setCompletion("MKT-42", true);
await kanera.comments.create("MKT-42", { body: "Shipped in 1.4.0." });

The SDK is published under the MIT license, deliberately more permissive than the Kanera server (Elastic License 2.0), so embedding it in your own application raises no license-policy questions.

Authenticate

Create an API key in Profile settings -> API keys or Workspace settings -> API, and read it from the environment rather than committing it. See choose an API key type for which kind fits your integration.

const kanera = new Kanera({ apiKey: process.env.KANERA_API_KEY! });

Self-hosting? Point the client at your own deployment:

const kanera = new Kanera({ apiKey, baseUrl: "https://api.your-kanera.example" });

Check the scope first

const { scope } = await kanera.session();

read means every mutation will be refused with a FORBIDDEN error. Checking once at start-up is cheaper than discovering it halfway through a migration.

What is on the client

NamespaceCovers
kanera.workspacesWorkspaces, their boards, members, and automations
kanera.boardsAccessible boards, opening a board, board cards and activity
kanera.listsList-level card operations: archive, move, set completion
kanera.cardsCards, plus .attachments, .checklists, and .bulk
kanera.commentsCard comments and reactions
kanera.notesNotes, backlinks, and note attachments
kanera.searchSearch across accessible content
kanera.workCross-board work, history, portfolio summaries, and Up next queues
kanera.httpThe raw request client, for anything not yet wrapped

Card references

Anywhere a card is named, pass a UUID, a human key such as MKT-42, or a canonical card URL. Keys are resolved once per client and cached, so repeating one across a bulk operation costs a single lookup.

A key prefix is unique only inside an organisation, and a personal credential can see several. A key visible in more than one is rejected, not guessed at — pass the canonical URL or the UUID to disambiguate.

Pagination

Every list endpoint is bounded; there is no "give me the whole board" call. The common integration bug is reading page one and treating it as the whole set, so iteration is the default shape:

for await (const board of kanera.boards.iterate()) { /* … */ }

const firstFifty = await kanera.comments.iterate("MKT-42").all(50);

// Or drive the pages yourself.
let cursor: string | undefined;
do {
const page = await kanera.comments.list("MKT-42", { cursor });
cursor = page.nextCursor ?? undefined;
} while (cursor);

Errors

import { KaneraApiError } from "@kanera/sdk";

try {
await kanera.cards.update("MKT-42", { title: "New" });
} catch (error) {
if (error instanceof KaneraApiError && error.isForbidden) {
// A read-only credential, or no Editor access to this board. Retrying will not help.
}
}

KaneraApiError carries the HTTP status, Kanera's stable error code, and guards for the cases worth branching on: isUnauthenticated, isForbidden, isNotFound, isRateLimited, isRetryable. Transport failures raise KaneraConnectionError.

Retries and idempotency

Reads and DELETE/PUT are retried automatically on rate limits and transient upstream failures, with exponential backoff, full jitter, and Retry-After honoured when Kanera sends it.

Mutations are not retried unless you supply an idempotency key, because a retry after an ambiguous failure would create a second card or post a second comment. Supply one and the API replays the first outcome instead of repeating the write:

await kanera.comments.create("MKT-42", { body: "Deployed." }, {
idempotencyKey: crypto.randomUUID(),
});

Verify webhooks

parseWebhook verifies the signature and returns the parsed event, or throws. It throws rather than returning a boolean so a handler cannot forget to check before using the payload:

import { parseWebhook } from "@kanera/sdk";

app.post("/kanera", async (req, res) => {
const event = await parseWebhook({
secret: process.env.KANERA_WEBHOOK_SECRET!,
payload: req.rawBody, // the exact bytes; re-serialised JSON will not verify
headers: req.headers,
});
// event.type, event.workspaceId, event.data
res.sendStatus(204);
});

The signature covers {timestamp}.{body}, and the timestamp is checked against a five-minute window — configurable — to bound replay. It uses Web Crypto, so it works in workers and edge runtimes as well as Node. See Webhooks for endpoint setup and the event catalog.

Options

new Kanera({
apiKey,
baseUrl: "https://api.kanera.app", // your own origin when self-hosting
timeoutMs: 30_000,
maxRetries: 2,
organisationId, // pin an identity-wide personal credential to one organisation
userAgent: "acme-sync/2.1", // appended to the SDK's own User-Agent
onRetry: ({ attempt, delayMs }) => log.warn({ attempt, delayMs }),
fetch: myFetch, // supply your own agent or proxy
});

Escape hatch

The API moves faster than the typed surface, and nothing is gated behind a wrapper:

const result = await kanera.http.post<MyType>("/api/v1/some/new/endpoint", body);

A note on browsers

The package runs in a browser, but using it there means shipping an API key to the client. Only do that in a trusted first-party context. For anything user-facing, call Kanera from your server.