Node.js
TypeScript-first server client for every resource, with auto-pagination and webhook verification.
Install
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.
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.
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.
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.
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
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.
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.
BillKit