npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@waotomatis/sdk

v0.3.0

Published

Official TypeScript SDK for WAOtomatis — headless WhatsApp (WABA Cloud API).

Downloads

124

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/sdk

Quickstart

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 again

Branch 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, install ws and 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_disconnected is permanent and surfaces immediately.)
  • Idempotency: GET/HEAD/OPTIONS/PUT/DELETE are retried by default. To make a POST (e.g. messages.send) safely retryable, pass an idempotencyKey — 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 your signal/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