@waotomatis/sdk
v0.3.0
Published
Official TypeScript SDK for WAOtomatis — headless WhatsApp (WABA Cloud API).
Downloads
124
Maintainers
Readme
@waotomatis/sdk
The official TypeScript SDK for WAOtomatis — headless WhatsApp on the WABA Cloud API. Send messages, upload media, stream events over WebSocket, verify webhooks, and manage your org — all fully typed, with auto-retries, timeouts, and auto-pagination built in.
Works in Node 18+, Bun, Deno, Cloudflare Workers, and the browser (uses the global fetch / WebSocket / Web Crypto).
npm install @waotomatis/sdk
# or: pnpm add @waotomatis/sdk • yarn add @waotomatis/sdk • bun add @waotomatis/sdkQuickstart
import { Waotomatis } from '@waotomatis/sdk';
const wao = new Waotomatis({ apiKey: process.env.WAOTOMATIS_API_KEY! });
// Pick a session (a connected WhatsApp number) and send a message.
const res = await wao.sessions('sess_123').messages.send({
to: '+15551234567',
type: 'text',
text: 'Hello from WAOtomatis 👋',
});
console.log(res.providerMessageId); // wamid.HBgL...Get an apiKey from your dashboard. List the sessions on your account with await wao.sessions.list().
Configuration
const wao = new Waotomatis({
apiKey: process.env.WAOTOMATIS_API_KEY!,
baseUrl: 'https://api.waotomatis.com', // default
maxRetries: 2, // default — transient failures only
timeoutMs: 60_000, // default per-request timeout
defaultHeaders: {}, // sent on every request
// fetch / webSocket — inject custom impls (tests, older Node, etc.)
});| Option | Default | Description |
| --- | --- | --- |
| apiKey | — | Required. Sent as Authorization: Bearer <apiKey>. |
| baseUrl | https://api.waotomatis.com | API origin. |
| maxRetries | 2 | Retries for 408/429/5xx and network errors, with exponential backoff + jitter, honoring Retry-After (capped at 60s). |
| timeoutMs | 60000 | Aborts the request after this many ms. |
| defaultHeaders | {} | Extra headers on every call (e.g. X-Org-Id). |
| fetch / webSocket | globals | Inject implementations for non-standard runtimes. |
Every method also takes a final RequestOptions argument for per-call overrides:
await wao.sessions('sess_123').messages.send(input, {
timeoutMs: 10_000,
maxRetries: 0,
idempotencyKey: 'order-4821-confirmation',
signal: AbortSignal.timeout(5_000),
headers: { 'X-Trace-Id': traceId },
});Sending messages
messages.send takes a discriminated union keyed on type. The compiler enforces exactly the fields each kind needs — wrong combinations don't compile, and autocomplete shows only the valid fields once you set type.
const messages = wao.sessions('sess_123').messages;Text
await messages.send({
to: '+15551234567',
type: 'text',
text: 'Your code is 6492. It expires in 10 minutes.',
previewUrl: true, // render a preview for the first URL in `text`
});Media — image / video / audio / document / sticker
Media is referenced by either an uploaded mediaId or a public link — exactly one. Supplying both (or neither) is a type error.
// By uploaded mediaId
await messages.send({
to: '+15551234567',
type: 'image',
mediaId: 'media_abc',
caption: 'Here is your receipt',
});
// By public link
await messages.send({
to: '+15551234567',
type: 'video',
link: 'https://cdn.example.com/demo.mp4',
caption: 'Product demo',
});
// Document with a filename
await messages.send({
to: '+15551234567',
type: 'document',
link: 'https://cdn.example.com/invoice.pdf',
fileName: 'invoice-0042.pdf',
});
// Audio as a voice note (PTT)
await messages.send({
to: '+15551234567',
type: 'audio',
mediaId: 'media_voice',
voice: true,
});
// Sticker (no caption)
await messages.send({ to: '+15551234567', type: 'sticker', mediaId: 'media_sticker' });Template
For business-initiated conversations. components carries header/body/button variables per the WABA template spec.
await messages.send({
to: '+15551234567',
type: 'template',
template: {
name: 'order_confirmation',
languageCode: 'en_US',
components: [
{ type: 'body', parameters: [{ type: 'text', text: 'Aïsha' }, { type: 'text', text: '#4821' }] },
],
},
});Interactive — buttons & lists
// Reply buttons
await messages.send({
to: '+15551234567',
type: 'interactive',
interactive: {
type: 'button',
bodyText: 'Did this resolve your issue?',
footerText: 'Support',
buttons: [
{ id: 'yes', title: 'Yes, thanks' },
{ id: 'no', title: 'Still stuck' },
],
},
});
// List picker
await messages.send({
to: '+15551234567',
type: 'interactive',
interactive: {
type: 'list',
headerText: 'Book a slot',
bodyText: 'Choose a time that works for you.',
listButton: 'View times',
sections: [
{
title: 'Morning',
rows: [
{ id: 'slot_9', title: '9:00 AM' },
{ id: 'slot_10', title: '10:00 AM', description: '1 spot left' },
],
},
],
},
});Replies, idempotency & read receipts
// Quote/reply to a prior message by its wamid
await messages.send({ to: '+15551234567', type: 'text', text: 'On it!', replyTo: 'wamid.XYZ' });
// Safe retries — the same key returns the original result (res.idempotent === true)
await messages.send({ to: '+15551234567', type: 'text', text: 'Receipt', idempotencyKey: 'rcpt-7781' });
// Mark an inbound message as read
await messages.markRead('wamid.ABC');Media uploads
Upload bytes once, reuse the mediaId across many sends.
const media = wao.sessions('sess_123').media;
// From bytes (Blob | ArrayBuffer | Uint8Array)
const file = await fs.promises.readFile('./receipt.png');
const { mediaId } = await media.upload(file, { fileName: 'receipt.png', mimeType: 'image/png' });
// From a URL — server fetches it for you
const { mediaId: id2 } = await media.uploadFromUrl('https://cdn.example.com/clip.mp4', 'video/mp4');
// Download inbound media bytes by id
const { data, mimeType } = await media.download('media_inbound_abc');
await wao.sessions('sess_123').messages.send({ to: '+15551234567', type: 'image', mediaId });Webhooks
Register a webhook (the signing secret is returned once — store it), then verify and parse incoming deliveries with constructEvent. It checks the HMAC-SHA256 signature against the exact raw body and returns a typed event discriminated on event.
Always pass the raw request body string, not a re-stringified object — re-serializing changes the bytes and breaks the HMAC.
import { constructEvent } from '@waotomatis/sdk';
// Register
const hook = await wao.sessions('sess_123').webhooks.create({
url: 'https://api.yourapp.com/webhooks/waotomatis',
events: ['message.received', 'message.updated', 'session.status'],
});
console.log(hook.secret); // store this now — not shown againBranch on the event — event.data is narrowed automatically:
const event = await constructEvent(rawBody, signatureHeader, secret);
switch (event.event) {
case 'message.received':
console.log(`From ${event.data.from}: ${event.data.text}`);
break;
case 'message.updated':
console.log(`${event.data.providerMessageId} → ${event.data.status}`);
break;
case 'session.status':
console.log(`Session is now ${event.data.status}`);
break;
}Framework handlers
Express — capture the raw body so the signature still matches:
import express from 'express';
import { constructEvent, WaotomatisError, WEBHOOK_SIGNATURE_HEADER } from '@waotomatis/sdk';
const app = express();
app.post(
'/webhooks/waotomatis',
express.text({ type: '*/*' }), // keep the body as a raw string
async (req, res) => {
try {
const event = await constructEvent(
req.body,
req.header(WEBHOOK_SIGNATURE_HEADER),
process.env.WAOTOMATIS_WEBHOOK_SECRET!,
);
// ... handle event ...
res.sendStatus(200);
} catch (err) {
if (err instanceof WaotomatisError && err.code === 'unauthorized') return res.sendStatus(401);
res.sendStatus(400);
}
},
);Next.js (App Router) — req.text() gives you the raw body:
// app/api/webhooks/waotomatis/route.ts
import { constructEvent, WEBHOOK_SIGNATURE_HEADER } from '@waotomatis/sdk';
export async function POST(req: Request) {
const raw = await req.text();
try {
const event = await constructEvent(
raw,
req.headers.get(WEBHOOK_SIGNATURE_HEADER),
process.env.WAOTOMATIS_WEBHOOK_SECRET!,
);
// ... handle event ...
return Response.json({ ok: true });
} catch {
return new Response('invalid signature', { status: 401 });
}
}Hono (great on Cloudflare Workers):
import { Hono } from 'hono';
import { constructEvent, WEBHOOK_SIGNATURE_HEADER } from '@waotomatis/sdk';
const app = new Hono();
app.post('/webhooks/waotomatis', async (c) => {
const raw = await c.req.text();
try {
const event = await constructEvent(raw, c.req.header(WEBHOOK_SIGNATURE_HEADER), c.env.WEBHOOK_SECRET);
// ... handle event ...
return c.json({ ok: true });
} catch {
return c.text('invalid signature', 401);
}
});Inspecting & replaying deliveries
const webhooks = wao.sessions('sess_123').webhooks;
// Auto-paginating delivery log
for await (const d of webhooks.deliveries('whk_1')) {
if (d.status === 'failed') console.log(d.id, d.responseCode, d.lastError);
}
// Re-send a past delivery
await webhooks.replay('whk_1', 'dlv_42');Realtime (WebSocket)
connect() opens an authenticated WebSocket and streams the same event envelope as webhooks. Handlers are typed per event name. Enable autoReconnect for resilient long-lived connections — it reconnects with backoff + jitter and re-fires a ready event so you can resync state.
const conn = wao.sessions('sess_123').connect({
autoReconnect: true,
maxReconnectAttempts: Infinity, // default
heartbeatIntervalMs: 30_000, // keepalive ping; 0 to disable
onOpen: () => console.log('live'),
onClose: (code, reason) => console.log('closed', code, reason),
onError: (err) => console.error(err),
});
conn.on('message.received', (evt) => console.log('inbound:', evt.data.text));
conn.on('message.updated', (evt) => console.log(evt.data.providerMessageId, evt.data.status));
conn.on('session.status', (evt) => console.log('status:', evt.data.status));
// Fired after a reconnect (evt.reconnected === true) — re-fetch any missed state here
conn.on('ready', () => console.log('stream is live again'));
// Catch-all
conn.on('*', (evt) => console.log('event:', evt.event));
// Clean shutdown — disables auto-reconnect
conn.close();conn.on(...) returns the connection, so calls chain. Use conn.off(event, handler) to unsubscribe and conn.readyState to check the socket state (0 connecting · 1 open · 2 closing · 3 closed).
On Node < 21 or runtimes without a global
WebSocket, installwsand pass it:new Waotomatis({ apiKey, webSocket: WebSocket }).
Auto-pagination
List methods that return many items (chats, chat history, contacts, webhook deliveries) return a PagePromise. It is both awaitable (resolves the first page) and async-iterable (auto-walks every cursor).
const session = wao.sessions('sess_123');
// Iterate every item across all pages
for await (const chat of session.chats.list()) {
console.log(chat.chatId, chat.lastText);
}
// Just the first page
const page = await session.chats.list({ limit: 50 });
console.log(page.data, page.hasMore, page.cursor);
// Collect everything into an array
const allContacts = await session.contacts.list().toArray();
// Chat history (newest first), explicit iterator
for await (const msg of session.chats.history('chat_42', { limit: 100 })) {
console.log(msg.direction, msg.type, msg.timestamp);
}Errors
Every failure is a subclass of WaotomatisError. Branch on the class for HTTP-shaped handling, or on the stable code for fine-grained logic. Each error carries code, message, status, and requestId (quote it in support tickets).
import {
WaotomatisError,
AuthenticationError, // 401
PermissionError, // 403
NotFoundError, // 404
ValidationError, // 409 / 422
RateLimitError, // 429 — has `retryAfter` (seconds)
ApiError, // 5xx
TimeoutError, // request exceeded timeoutMs / aborted
ConnectionError, // network failure before a response
} from '@waotomatis/sdk';
try {
await wao.sessions('sess_123').messages.send({ to, type: 'text', text: 'hi' });
} catch (err) {
if (err instanceof RateLimitError) {
await sleep((err.retryAfter ?? 1) * 1000);
} else if (err instanceof ValidationError) {
console.error('Bad request:', err.message);
} else if (err instanceof WaotomatisError && err.code === 'session_disconnected') {
// reconnect the session, then retry
} else if (err instanceof WaotomatisError) {
console.error(`[${err.status}] ${err.code}: ${err.message} (requestId: ${err.requestId})`);
} else {
throw err;
}
}Stable codes include unauthorized, forbidden_scope, insufficient_permissions, validation_failed, session_not_found, session_disconnected, message_not_found, media_not_found, unsupported_message_type, send_failed, rate_limited, meta_api_error, plus the client-side timeout and connection_error. Unknown future codes still type-check.
Retries & timeouts
The client automatically retries transient failures with exponential backoff + jitter:
- Retried statuses:
408, 429, 500, 502, 503, 504, plus network and timeout errors. (409 session_disconnectedis permanent and surfaces immediately.) - Idempotency:
GET/HEAD/OPTIONS/PUT/DELETEare retried by default. To make aPOST(e.g.messages.send) safely retryable, pass anidempotencyKey— the server dedupes it, and retries return the original result. Multipart uploads (media.upload) are never auto-retried, since a streamed body can't be re-sent. Retry-After(seconds or HTTP-date) is respected for the backoff delay, capped at 60s. The backoff wait honors yoursignal/timeoutMs, so aborting interrupts it immediately.
// Tune globally
const wao = new Waotomatis({ apiKey, maxRetries: 4, timeoutMs: 30_000 });
// Or per call
await wao.sessions(id).messages.send(input, {
maxRetries: 0,
timeoutMs: 5_000,
idempotencyKey: 'order-4821',
signal: controller.signal, // your own AbortController; combined with the timeout
});Account & org management
With an API key, the client exposes the control-plane of the key's own organization:
await wao.org.get(); // active org
await wao.teams.list(); // teams
await wao.members.list(); // members
await wao.keys.create({ name: 'CI key', role: 'member' }); // plaintext key returned once
await wao.usage.get(); // usage totals (last 30 days)
await wao.tickets.create({ subject: 'Help', body: 'Question about templates' });Creating organizations or resolving the signed-in user are dashboard actions — manage them at app.waotomatis.com. The SDK is scoped to the API key's organization.
TypeScript
Everything is fully typed. Import domain types and the raw OpenAPI types as needed:
import type {
Session,
SendMessageInput,
SendMessageResult,
WebhookEvent,
Chat,
Contact,
components, // raw OpenAPI schema types
} from '@waotomatis/sdk';License
MIT
