TypeScript SDK
A typed method per op, safe retries with idempotency keys, typed errors and a webhook signature verifier.
@meterapp/wabi-sdk is the TypeScript client for Wabi: one typed method per op, generated from the op catalog, so its inputs match the API reference exactly. Zero runtime dependencies; it runs in Node 20+, edge runtimes and browsers (keep the API key on a server). Prefer another language? Generate a client from /openapi.json; every op is POST /api/v1/{op} with a JSON body.
npm install @meterapp/wabi-sdkSend from the sandbox
Create an API key in Integrations (or run npx @meterapp/wabi login) and export it as WABI_API_KEY. WABI_HOST overrides the origin. This sends the approved hello_world template to a sandbox test customer:
import { Wabi } from "@meterapp/wabi-sdk";
const wabi = new Wabi(); // reads WABI_API_KEY
const sandbox = await wabi.getSandbox<{ phone_number: { id: string }; test_recipients: { wa_id: string }[] }>();const sent = await wabi.sendMessage({ phone_number_id: sandbox.phone_number.id, to: sandbox.test_recipients[0].wa_id, type: "template", template: { name: "hello_world", language: "en_US" },});console.log(sent);Every op
// Every op is a typed method (camelCase) and also callable by name:await wabi.getCosts({ days: 30 });await wabi.call("get_costs", { days: 30 });
// Narrow the result type where you need it:const { plan } = await wabi.getUsage<{ plan: { messages: { used: number; included: number | null } } }>();Inputs are typed and documented from each op's schema; results are plain JSON objects. The package is regenerated from the live catalog before each release, so it never names an op the API does not have.
Retries and idempotency
Rate limits (429), server errors and network failures are retried with exponential backoff (twice by default; maxRetries changes it), honouring Retry-After. Reads are always retried. Writes are retried only when they are safe: ops that take an idempotency_key (such as send_message) get one automatically, the same on every attempt.
// Retries of a send never send twice: the SDK fills idempotency_key once per call// and reuses it on every retry. Pass your own to make a retry from another process safe too.await wabi.sendMessage(input, { idempotencyKey: `order-${order.id}-shipped` });Errors
A failed call throws WabiError with the HTTP status, the error code, the request id (quote it when reporting an issue) and the whole problem body.
import { Wabi, WabiError } from "@meterapp/wabi-sdk";
try { await wabi.sendMessage({ phone_number_id, to, type: "text", text: { body: "Hi!" } });} catch (error) { if (error instanceof WabiError) { // error.status, error.code ("window_closed", "plan_limit_reached"…), error.requestId, error.problem if (error.status === 402) console.log("Upgrade:", error.problem.checkout_url); } throw error;}Verify webhooks
verifyWebhook checks a delivery's Standard Webhooks signature with Web Crypto and returns the parsed event; it throws on a bad signature or a timestamp older than five minutes. isValidWebhook returns a boolean instead.
import { verifyWebhook } from "@meterapp/wabi-sdk/webhooks";
export async function POST(request: Request) { // Verify the raw body before parsing it. const event = await verifyWebhook(await request.text(), request.headers, process.env.WABI_WEBHOOK_SECRET!); if (event.type === "message.received") { // dedupe on event.id, answer fast, do slow work afterwards } return new Response(null, { status: 204 });}