BillKit/Docs Console
SDK

Node.js

TypeScript-first server client for every resource, with auto-pagination and webhook verification.

v0.4.0 Node 20+, Bun, Deno, Cloudflare Workers Beta Source

Install

npm install @billkit-eu/sdk

Anything with native fetch and crypto.subtle runs it: Node 20 and up, Bun, Deno, Cloudflare Workers. ESM and CJS builds ship together, with types.

Initialise

The constructor takes an options object. Leave apiKey out and it reads BILLKIT_API_KEY from the environment. In serverless runtimes build the client at module scope so connections survive between invocations.

typescript
import { BillKit } from "@billkit-eu/sdk";

export const client = new BillKit({
apiKey: process.env.BILLKIT_API_KEY,
baseUrl: "https://api.billkit.eu",
timeoutMs: 30_000,
});

Making calls

One accessor per resource family, each mirroring the verbs on /v1/<resource>: client.customers, products, prices, checkoutSessions, oneShotPayments, subscriptions, refunds, disputes, webhookEndpoints, events, tenant, coupons, taxRates, invoices, auditLogs, payments, billingPortalSessions.

Methods are generic, so you hand in your own response type rather than accepting whatever the server sent. The SDK ships no runtime schemas; most callers forward the JSON to their own data layer unchanged.

typescript
interface Customer {
id: string;
email: string | null;
created: number;
}

const customer = await client.customers.retrieve<Customer>("cus_9XKp2vQ1");

Auto-pagination

Every list-returning resource has an iter() async iterator that walks the has_more and starting_after cursor protocol for you. Paging is forward-only, so to go back, keep the cursors you have already used.

typescript
for await (const customer of client.customers.iter()) {
console.log(customer);
}

// Filter at the server and raise the page size to cut round-trips.
for await (const evt of client.events.iter({ type: "subscription.created", pageSize: 100 })) {
await handle(evt);
}

Idempotency and retries

Connection errors, 5xx and short Retry-After 429s are retried with jittered backoff. Every mutating call carries an auto-generated Idempotency-Key, so a retry replays the original response instead of charging twice. Pass idempotencyKey yourself to make that hold across process restarts.

409 idempotency_in_progress is retried too, reusing the same key. It means an earlier request with that key is still in flight, and retrying with a fresh key is what turns one charge into two. Every other 409 fails immediately.

Errors

Errors are chosen by HTTP status, not by the envelope’s type: the status is the field the API cannot get wrong. All of them extend BillKitError and carry statusCode, requestId and the envelope’s type, code and param.

typescript
import { ResourceMissingError, RateLimitError, BillKitError } from "@billkit-eu/sdk";

try {
await client.customers.retrieve("cus_gone");
} catch (err) {
if (err instanceof ResourceMissingError) return null;
if (err instanceof RateLimitError) return retryIn(err.retryAfter);
if (err instanceof BillKitError) throw new Error(`BillKit ${err.statusCode}: ${err.message}`);
throw err;
}

401 is AuthenticationError, 403 PermissionError, 404 ResourceMissingError, 409 ConflictError, 429 RateLimitError, other 4xx InvalidRequestError, 5xx ServerError, and a failed connection APIConnectionError.

Webhooks

typescript
import { verifyWebhookSignature, WebhookVerificationError } from "@billkit-eu/sdk";

try {
const event = await verifyWebhookSignature({
  payload: rawBody, // string or Uint8Array, never the parsed object
  signatureHeader: request.headers.get("BillKit-Signature"),
  secret: process.env.BILLKIT_WEBHOOK_SECRET,
});
await handle(event);
} catch (err) {
if (err instanceof WebhookVerificationError) return new Response("invalid signature", { status: 400 });
throw err;
}

Constant-time HMAC compare and a five-minute timestamp tolerance for replay protection. Pass toleranceSeconds to change it.

Logging

Silent by default: the SDK ships no logger and picks no destination, so it cannot take over your application’s output. Pass one to opt in.

typescript
const client = new BillKit({ apiKey, logger: console });

You get one debug per attempt and per response, and one warn per retry. Your API key, request and response bodies and the query string are never logged: only the method, path, status, duration and request id. console, and any pino or winston child logger, satisfies the interface as-is.