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

@abdelrahmannasr/wa-cloud-api

v1.0.0

Published

Zero-dependency, type-safe TypeScript SDK for the Meta WhatsApp Cloud API

Readme

@abdelrahmannasr/wa-cloud-api

npm version CI codecov License: MIT Node.js >= 18 TypeScript 5.x npm downloads PRs Welcome semantic-release: angular

Overview

A comprehensive, zero-dependency, type-safe TypeScript SDK for the Meta WhatsApp Cloud API. Built for Node.js 18+, this SDK provides a unified client interface for sending messages, managing media, creating templates, handling webhooks, managing phone numbers, and coordinating multiple WhatsApp Business Accounts (WABAs). Perfect for building WhatsApp integrations, chatbots, and campaign systems.

Features

  • Messages — Send text, media (images, videos, audio, documents, stickers), locations, contacts, reactions, interactive buttons/lists, and templates
  • Media — Upload, download, retrieve URLs, and delete media assets with client-side validation
  • Templates — Create, list, update, and delete message templates with a fluent TemplateBuilder API
  • Flows — Create, publish, update, deprecate, and delete WhatsApp Flows; send flow messages; receive flow completions as typed events
  • Webhooks — Parse incoming events (including flow completions), verify signatures, and integrate with Express or Next.js App Router
  • Phone Numbers — List, manage business profiles, request verification codes, and register/deregister numbers
  • Multi-Account — Manage multiple WABAs with distribution strategies (round-robin, weighted, sticky), broadcast messaging with concurrency control, and dynamic account management

Installation

# npm
npm install @abdelrahmannasr/wa-cloud-api

# pnpm
pnpm add @abdelrahmannasr/wa-cloud-api

# yarn
yarn add @abdelrahmannasr/wa-cloud-api

Quick Start

import { WhatsApp } from '@abdelrahmannasr/wa-cloud-api';

// Initialize the client
const wa = new WhatsApp({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
});

// Send a text message
try {
  const result = await wa.messages.sendText({
    to: '1234567890',
    body: 'Hello from WhatsApp Cloud API!',
  });

  console.log('Message sent:', result.data.messages[0].id);
} catch (error) {
  // See Error Handling section for typed error handling patterns
  console.error('Failed to send message:', error);
}

// Clean up
wa.destroy();

Configuration

The WhatsApp client accepts a configuration object with the following options:

| Option | Type | Required | Default | Description | | ---------------------------- | --------- | -------- | ------------------------------ | ----------------------------------------------------- | | accessToken | string | Yes | — | Meta access token for authentication | | phoneNumberId | string | Yes | — | WhatsApp phone number ID | | businessAccountId | string | No | — | WhatsApp Business Account ID (required for templates) | | apiVersion | string | No | 'v21.0' | Graph API version | | baseUrl | string | No | 'https://graph.facebook.com' | API base URL | | logger | Logger | No | — | Custom logger instance | | rateLimitConfig.maxTokens | number | No | 80 | Max tokens in bucket | | rateLimitConfig.refillRate | number | No | 80 | Tokens refilled per second | | rateLimitConfig.enabled | boolean | No | true | Enable/disable rate limiting | | retryConfig.maxRetries | number | No | 3 | Maximum retry attempts | | retryConfig.baseDelayMs | number | No | 1000 | Base delay between retries (ms) | | retryConfig.maxDelayMs | number | No | 30000 | Maximum delay cap (ms) | | retryConfig.jitterFactor | number | No | 0.2 | Jitter randomization factor (0-1) | | timeoutMs | number | No | 30000 | Request timeout in milliseconds | | appSecret | string | No | — | App secret for webhook signature verification | | webhookVerifyToken | string | No | — | Webhook verify token |

Messages

Send various types of messages using the messages module:

// Text message
await wa.messages.sendText({
  to: '1234567890',
  body: 'Hello! 👋',
  previewUrl: true, // Enable URL preview
});

// Image message
await wa.messages.sendImage({
  to: '1234567890',
  media: { link: 'https://example.com/image.jpg' },
  caption: 'Check out this image!',
});

// Template message
await wa.messages.sendTemplate({
  to: '1234567890',
  templateName: 'hello_world',
  language: 'en_US',
});

// Interactive buttons
await wa.messages.sendInteractiveButtons({
  to: '1234567890',
  body: 'Choose an option:',
  buttons: [
    { type: 'reply', reply: { id: 'btn1', title: 'Option 1' } },
    { type: 'reply', reply: { id: 'btn2', title: 'Option 2' } },
  ],
});

// Mark message as read
await wa.messages.markAsRead({
  messageId: 'wamid.ABC123...',
});

Additional message types:

// Video message
await wa.messages.sendVideo({
  to: '1234567890',
  media: { id: 'media_id_123' },
  caption: 'Watch this video',
});

// Audio message
await wa.messages.sendAudio({
  to: '1234567890',
  media: { link: 'https://example.com/audio.mp3' },
});

// Document message
await wa.messages.sendDocument({
  to: '1234567890',
  media: { link: 'https://example.com/report.pdf' },
  filename: 'report.pdf',
  caption: 'Monthly report',
});

// Sticker message
await wa.messages.sendSticker({
  to: '1234567890',
  media: { id: 'sticker_media_id' },
});

// Location message
await wa.messages.sendLocation({
  to: '1234567890',
  longitude: -122.4194,
  latitude: 37.7749,
  name: 'San Francisco',
  address: 'San Francisco, CA',
});

// Contacts message
await wa.messages.sendContacts({
  to: '1234567890',
  contacts: [
    {
      name: { formatted_name: 'Jane Doe' },
      phones: [{ phone: '+0987654321', type: 'WORK' }],
    },
  ],
});

// Reaction (emoji react to an existing message)
await wa.messages.sendReaction({
  to: '1234567890',
  messageId: 'wamid.ABC123...',
  emoji: '\u{1F44D}', // Use empty string to remove reaction
});

// Interactive list message
await wa.messages.sendInteractiveList({
  to: '1234567890',
  body: 'Select a product:',
  buttonText: 'View options',
  sections: [
    {
      title: 'Products',
      rows: [
        { id: 'p1', title: 'Widget', description: 'A useful widget' },
        { id: 'p2', title: 'Gadget', description: 'A fancy gadget' },
      ],
    },
  ],
  header: 'Our Catalog',
  footer: 'Reply STOP to opt out',
});

Media

Upload, download, and manage media files:

import { readFileSync } from 'fs';

// Upload media
const uploadResult = await wa.media.upload({
  file: readFileSync('./image.jpg'),
  mimeType: 'image/jpeg',
  category: 'image',
  filename: 'image.jpg',
});

const mediaId = uploadResult.data.id;

// Get media URL
const urlResult = await wa.media.getUrl(mediaId);
console.log('Download URL:', urlResult.data.url);

// Download media
const downloadResult = await wa.media.download(urlResult.data.url);
const buffer = Buffer.from(downloadResult.data);

// Delete media
await wa.media.delete(mediaId);

Supported Media Types

The SDK validates MIME types and file sizes client-side before uploading:

| Category | MIME Types | Max Size | | -------------------- | ---------------------------------------------------------------------------- | -------- | | image | image/jpeg, image/png | 5 MB | | video | video/mp4, video/3gpp | 16 MB | | audio | audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg | 16 MB | | document | application/pdf, text/plain, text/csv, MS Office, OpenDocument formats | 100 MB | | sticker (static) | image/webp | 500 KB | | sticker (animated) | image/webp | 1 MB |

// Upload a sticker (stickerType is required for stickers)
await wa.media.upload({
  file: stickerBuffer,
  mimeType: 'image/webp',
  category: 'sticker',
  stickerType: 'static', // or 'animated'
});

Templates

Create and manage message templates:

import { TemplateBuilder } from '@abdelrahmannasr/wa-cloud-api';

// List templates
const templates = await wa.templates.list({ limit: 10 });

// Get specific template
const template = await wa.templates.get('hello_world');

// Create template with TemplateBuilder
const newTemplate = new TemplateBuilder()
  .setName('order_confirmation')
  .setLanguage('en_US')
  .setCategory('UTILITY')
  .addBody('Your order {{1}} has been confirmed. Total: {{2}}.')
  .addQuickReplyButton('Track Order')
  .build();

await wa.templates.create(newTemplate);

// Full template with all builder options
const promoTemplate = new TemplateBuilder()
  .setName('summer_sale')
  .setLanguage('en_US')
  .setCategory('MARKETING')
  .allowCategoryChange(true)
  .addHeaderText('Summer Sale!')
  .addBody('Hi {{1}}, enjoy {{2}}% off all items this week.')
  .addFooter('Terms and conditions apply')
  .addQuickReplyButton('Shop Now')
  .addQuickReplyButton('Not Interested')
  .addUrlButton('View Catalog', 'https://example.com/catalog/{{1}}')
  .addPhoneNumberButton('Call Support', '+1234567890')
  .build();

await wa.templates.create(promoTemplate);

// Update template components
await wa.templates.update('template_id', [
  { type: 'BODY', text: 'Updated: Your order {{1}} has been confirmed.' },
]);

// Delete template
await wa.templates.delete('old_template');

TemplateBuilder methods: setName(), setLanguage(), setCategory(), allowCategoryChange(), addHeaderText(), addHeaderMedia(format, example?), addBody(text, example?), addFooter(), addQuickReplyButton() (max 3), addUrlButton() (max 2), addPhoneNumberButton() (max 1).

Flows

Create and manage WhatsApp Flows for interactive forms, surveys, and guided journeys:

Send a Flow

// Send a published flow to a user
await wa.messages.sendFlow({
  to: '1234567890',
  body: 'Please complete your appointment booking.',
  flowCta: 'Book Now',
  flowId: '9876543210',
});

// Test a draft flow before publishing
await wa.messages.sendFlow({
  to: '1234567890',
  body: 'Preview the onboarding flow',
  flowCta: 'Start',
  flowId: '9876543210',
  mode: 'draft',
});

// Pre-populate initial screen data
await wa.messages.sendFlow({
  to: '1234567890',
  body: 'Review your profile',
  flowCta: 'Continue',
  flowId: '9876543210',
  flowActionPayload: {
    screen: 'EDIT_PROFILE',
    data: { name: 'Alice', email: '[email protected]' },
  },
});

Receive Flow Completions

Flow completions arrive as a dedicated FlowCompletionEvent via the onFlowCompletion callback (NOT via onMessage):

const handler = wa.webhooks.createHandler({
  onMessage: async (event) => {
    // Text, images, button/list replies — unchanged
  },
  onFlowCompletion: async (event) => {
    // Deduplicate (Meta retries on errors)
    if (await db.isProcessed(event.messageId)) return;
    await db.markProcessed(event.messageId);

    // event.response is the parsed form data (or {} if malformed)
    // event.responseJson is the raw string, preserved exactly
    await saveSubmission(event.contact.waId, event.response);
  },
});

Flow Lifecycle (CRUD)

Requires businessAccountId in the client config:

const wa = new WhatsApp({
  accessToken: '...',
  phoneNumberId: '...',
  businessAccountId: 'YOUR_WABA_ID',
});

// Create a flow
const created = await wa.flows.create({
  name: 'customer_onboarding',
  categories: ['SIGN_UP'],
});

// Upload flow JSON (accepts string or object — SDK stringifies objects)
await wa.flows.updateAssets(created.data.id, {
  flow_json: { version: '3.0', screens: [/* ... */] },
});

// Publish
await wa.flows.publish(created.data.id);

// List, get, update metadata, preview, deprecate, delete
const list = await wa.flows.list({ limit: 10 });
const flow = await wa.flows.get('flow_id');
await wa.flows.updateMetadata('flow_id', { name: 'new_name' });
const preview = await wa.flows.getPreview('flow_id');
await wa.flows.deprecate('flow_id');
await wa.flows.delete('draft_flow_id'); // Only draft flows can be deleted

Multi-Account Broadcast with Flows

Flow IDs are scoped to a single WABA. When broadcasting across accounts, maintain a per-account flow ID mapping:

const flowIdByAccount = {
  us: 'flow_id_in_us_account',
  eu: 'flow_id_in_eu_account',
};

const result = await manager.broadcast(
  ['15551234567', '442071234567'],
  (account, recipient) => account.messages.sendFlow({
    to: recipient,
    body: 'Complete your registration',
    flowCta: 'Get Started',
    flowId: flowIdByAccount[account.name as keyof typeof flowIdByAccount],
  }),
);

Flows methods: list(), get(), create(), updateMetadata(), updateAssets(), publish(), deprecate(), delete(), getPreview().

Commerce & Catalogs

Send product messages, receive order notifications, and manage your product catalog programmatically. All catalog operations require businessAccountId in the client config.

Send a Single Product Message

// Display one product card to a recipient
await wa.messages.sendProduct({
  to: '1234567890',
  catalogId: 'CATALOG_ID',
  productRetailerId: 'SKU-001',
  body: 'Check out this item — just restocked!',
  footer: 'Limited stock',
});

Send a Multi-Product List

Up to 30 products across up to 10 named sections (validated client-side before the API call):

await wa.messages.sendProductList({
  to: '1234567890',
  catalogId: 'CATALOG_ID',
  header: "Today's Specials",
  body: "Here's our curated selection.",
  sections: [
    {
      title: 'Beverages',
      productRetailerIds: ['cola-001', 'juice-002'],
    },
    {
      title: 'Snacks',
      productRetailerIds: ['chips-001', 'nuts-002'],
    },
  ],
});

Send a Catalog Message

Invite the recipient to browse your entire catalog. Optionally pin a featured product as a thumbnail:

await wa.messages.sendCatalogMessage({
  to: '1234567890',
  body: 'Browse our full collection.',
  footer: 'Free shipping on orders over $50',
  thumbnailProductRetailerId: 'featured-item-001', // optional
});

Receive Order Notifications

When a recipient submits a cart, the SDK fires a dedicated OrderEvent via onOrder. This event is never delivered to the generic onMessage callback:

wa.webhooks.onOrder(async (event) => {
  // Deduplicate using the stable platform message ID
  if (await db.isProcessed(event.messageId)) return;
  await db.markProcessed(event.messageId);

  console.log(`Order from ${event.from}: ${event.items.length} item(s)`);
  for (const item of event.items) {
    console.log(`  ${item.product_retailer_id} × ${item.quantity} @ ${item.item_price} ${item.currency}`);
  }

  // Reply to confirm
  await wa.messages.sendText({
    to: event.from,
    body: `Thanks! We received your order and will process it shortly.`,
  });
});

event.raw preserves the original JSON-stringified payload for storage or auditing. If product_items is malformed, event.items is [] and event.raw is still preserved.

Template lifecycle events

React to template approvals, rejections, and quality changes without polling the templates API. Events arrive on the same webhook URL, routed automatically by the SDK:

wa.webhooks
  .onTemplateStatus(async (event) => {
    if (event.status === 'APPROVED') {
      await db.markTemplateLive(event.templateId);
    } else if (event.status === 'REJECTED') {
      await alerts.notify({
        template: event.templateName,
        language: event.language,
        reason: event.reason ?? 'no reason provided',
      });
    }
  })
  .onTemplateQuality(async (event) => {
    // previousScore is undefined for first-time ratings
    if (event.newScore === 'RED') {
      await throttle.pauseCampaign(event.templateId);
    }
  });

Template events use a WABA-scoped metadata.businessAccountId (sourced from entry.id) instead of phoneNumberId. The status and newScore fields preserve unknown platform-added values verbatim so your code doesn't break when Meta adds new states. Requires subscribing to message_template_status_update and message_template_quality_update in the Meta App Dashboard.

Catalog Management

Create, update, and delete products from code. Requires businessAccountId:

// List catalogs connected to the WABA
const { data } = await wa.catalog.listCatalogs();

// Create a product (strict — throws ConflictError on duplicate retailer_id)
try {
  await wa.catalog.createProduct('CATALOG_ID', {
    retailer_id: 'SKU-001',
    name: 'Wireless Headphones',
    image_url: 'https://example.com/sku-001.jpg',
    price: 4999,       // integer minor units ($49.99)
    currency: 'USD',
    availability: 'in stock',
  });
} catch (err) {
  if (err instanceof ConflictError) {
    // Fall back to upsert (create-or-update)
    await wa.catalog.upsertProduct('CATALOG_ID', { /* same payload */ });
  }
}

// Partial update
await wa.catalog.updateProduct('PRODUCT_ID', { price: 3999 });

// Delete
await wa.catalog.deleteProduct('PRODUCT_ID');

Catalog methods: listCatalogs(), getCatalog(), listProducts(), getProduct(), createProduct(), upsertProduct(), updateProduct(), deleteProduct().

Limitations

  • No bulk product mutations — single-product CRUD only; compose multiple calls for bulk sync.
  • No product image hostingimage_url must be a publicly accessible HTTPS URL hosted by the consumer (own CDN, S3, Cloudflare R2, etc.).
  • No order acknowledgement messages — reply to orders with any existing message type (e.g., sendText); dedicated sendOrderStatusMessage is out of scope for v0.4.0.
  • No lookup by retailer_id — use listProducts with a filter to find a product by its retailer ID, or use upsertProduct for create-or-update semantics.
  • Catalog IDs are WABA-scoped — when broadcasting across multiple accounts in different WABAs, maintain a per-account catalog ID mapping.

Webhooks

Handle incoming webhook events from WhatsApp:

Express.js:

import express from 'express';
import { createExpressMiddleware } from '@abdelrahmannasr/wa-cloud-api/webhooks';

const app = express();

app.use(
  '/webhook',
  createExpressMiddleware(
    {
      appSecret: process.env.WHATSAPP_APP_SECRET!,
      verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
    },
    {
      onMessage: (event) => {
        console.log('Message received:', event.message.text?.body);
      },
      onStatus: (event) => {
        console.log('Status update:', event.status.status);
      },
      onError: (event) => {
        console.error('Error:', event.error);
      },
    },
  ),
);

app.listen(3000);

Next.js App Router:

// app/api/webhook/route.ts
import { createNextRouteHandler } from '@abdelrahmannasr/wa-cloud-api/webhooks';

const { GET, POST } = createNextRouteHandler(
  {
    appSecret: process.env.WHATSAPP_APP_SECRET!,
    verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
  },
  {
    onMessage: (event) => {
      console.log('Message received:', event.message.text?.body);
    },
    onStatus: (event) => {
      console.log('Status update:', event.status.status);
    },
  },
);

export { GET, POST };

Unified Client:

When using the WhatsApp client, webhooks are available via wa.webhooks:

const wa = new WhatsApp({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  appSecret: process.env.WHATSAPP_APP_SECRET!,
  webhookVerifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
});

// Parse webhook payload into typed events
const events = wa.webhooks.parse(webhookPayload);
for (const event of events) {
  if (event.type === 'message') {
    console.log('Message from:', event.message.from);
  }
}

// Verify webhook subscription (GET endpoint)
const challenge = wa.webhooks.verify(queryParams);

// Verify payload signature (POST endpoint)
wa.webhooks.verifySignature(rawBody, signatureHeader);

// Or create middleware directly from the unified client
app.use(
  '/webhook',
  wa.webhooks.createExpressMiddleware({
    onMessage: (event) => console.log('Message:', event.message.text?.body),
  }),
);

Phone Numbers

Manage phone numbers and business profiles:

// List phone numbers
const numbers = await wa.phoneNumbers.list();

// Get phone number details
const number = await wa.phoneNumbers.get('phone_number_id');

// Get business profile
const profile = await wa.phoneNumbers.getBusinessProfile('phone_number_id');

// Update business profile
await wa.phoneNumbers.updateBusinessProfile('phone_number_id', {
  description: 'Your trusted business partner',
  websites: ['https://example.com'],
});

// Request verification code
await wa.phoneNumbers.requestVerificationCode('phone_number_id', {
  code_method: 'SMS',
  language: 'en',
});

// Verify code
await wa.phoneNumbers.verifyCode('phone_number_id', { code: '123456' });

// Register a verified phone number
await wa.phoneNumbers.register('phone_number_id', {
  pin: '123456', // Two-step verification PIN
});

// Deregister a phone number
await wa.phoneNumbers.deregister('phone_number_id');

Multi-Account

Manage multiple WhatsApp Business Accounts:

import { WhatsAppMultiAccount } from '@abdelrahmannasr/wa-cloud-api';

const multiAccount = new WhatsAppMultiAccount({
  // Shared base config
  retryConfig: { maxRetries: 3 },

  // Per-account configurations
  accounts: [
    {
      name: 'account1',
      accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
      phoneNumberId: process.env.ACCOUNT1_PHONE_NUMBER_ID!,
    },
    {
      name: 'account2',
      accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
      phoneNumberId: process.env.ACCOUNT2_PHONE_NUMBER_ID!,
    },
  ],
});

// Send via specific account
const client1 = multiAccount.get('account1');
await client1.messages.sendText({
  to: '1234567890',
  body: 'Hello from account 1!',
});

// Lookup by phone number ID
const client2 = multiAccount.get(process.env.ACCOUNT2_PHONE_NUMBER_ID!);

// Clean up all accounts
multiAccount.destroy();

Dynamic Account Management

Add, remove, and query accounts at runtime:

// Check if an account exists (by name or phone number ID)
if (!manager.has('marketing')) {
  // Add a new account dynamically
  manager.addAccount({
    name: 'marketing',
    accessToken: 'TOKEN_M',
    phoneNumberId: 'PHONE_M',
    businessAccountId: 'WABA_M',
  });
}

// List all registered accounts
const accounts = manager.getAccounts();
for (const [name, config] of accounts) {
  console.log(`${name}: ${config.phoneNumberId}`);
}

// Remove an account (destroys its client if created)
manager.removeAccount('marketing');

Distribution Strategies

Automatically distribute sends across accounts using built-in strategies:

import { WhatsAppMultiAccount, RoundRobinStrategy } from '@abdelrahmannasr/wa-cloud-api';

const manager = new WhatsAppMultiAccount({
  strategy: new RoundRobinStrategy(),
  accounts: [
    { name: 'account-a', accessToken: 'TOKEN_A', phoneNumberId: 'PHONE_A' },
    { name: 'account-b', accessToken: 'TOKEN_B', phoneNumberId: 'PHONE_B' },
    { name: 'account-c', accessToken: 'TOKEN_C', phoneNumberId: 'PHONE_C' },
  ],
});

// Each call cycles: A → B → C → A → B → ...
const wa = manager.getNext();
await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });

Weighted Distribution

Route traffic proportionally based on per-account weights:

import { WhatsAppMultiAccount, WeightedStrategy } from '@abdelrahmannasr/wa-cloud-api';

const manager = new WhatsAppMultiAccount({
  strategy: new WeightedStrategy(
    new Map([
      ['enterprise', 80], // gets ~80% of traffic
      ['business-1', 10], // gets ~10% of traffic
      ['business-2', 10], // gets ~10% of traffic
    ]),
  ),
  accounts: [
    { name: 'enterprise', accessToken: 'TOKEN_E', phoneNumberId: 'PHONE_E' },
    { name: 'business-1', accessToken: 'TOKEN_1', phoneNumberId: 'PHONE_1' },
    { name: 'business-2', accessToken: 'TOKEN_2', phoneNumberId: 'PHONE_2' },
  ],
});

const wa = manager.getNext();
await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });

Sticky Routing

Ensure the same recipient always routes to the same account for conversation continuity:

import { WhatsAppMultiAccount, StickyStrategy } from '@abdelrahmannasr/wa-cloud-api';

const manager = new WhatsAppMultiAccount({
  strategy: new StickyStrategy(),
  accounts: [
    { name: 'account-a', accessToken: 'TOKEN_A', phoneNumberId: 'PHONE_A' },
    { name: 'account-b', accessToken: 'TOKEN_B', phoneNumberId: 'PHONE_B' },
  ],
});

// Same recipient always routes to the same account
const wa = manager.getNext('1234567890');
await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });

Stickiness is not stable across account-set mutations. StickyStrategy uses a simple hash(recipient) % accountNames.length mapping, so calling addAccount() or removeAccount() shifts the modulo result and reroutes most recipients to a different account. If your deployment adds or removes accounts while conversations are in flight and you need routing to survive those changes, implement a custom DistributionStrategy using rendezvous (HRW) or consistent hashing — they rebind only ~1/N of recipients on mutation.

Broadcast

Send a message to many recipients in parallel, distributed across accounts:

const recipients = ['1111111111', '2222222222', '3333333333', '4444444444'];

const result = await manager.broadcast(
  recipients,
  async (wa, to) => wa.messages.sendText({ to, body: 'Campaign message!' }),
  { concurrency: 10 }, // limit to 10 concurrent sends
);

console.log(`Sent: ${result.successes.length}, Failed: ${result.failures.length}`);

for (const failure of result.failures) {
  console.error(`Failed to send to ${failure.recipient}:`, failure.error);
}

Custom Strategy

Implement the DistributionStrategy interface for custom routing logic:

import type { DistributionStrategy } from '@abdelrahmannasr/wa-cloud-api';

class PriorityStrategy implements DistributionStrategy {
  select(accountNames: readonly string[], _recipient?: string): string {
    return accountNames[0]; // Always prefer the first account
  }
}

const manager = new WhatsAppMultiAccount({
  strategy: new PriorityStrategy(),
  accounts: [
    { name: 'primary', accessToken: 'TOKEN_P', phoneNumberId: 'PHONE_P' },
    { name: 'fallback', accessToken: 'TOKEN_F', phoneNumberId: 'PHONE_F' },
  ],
});

Examples

Complete, runnable examples are available in the examples/ directory. Each example demonstrates a specific feature with inline documentation and environment variable setup:

  • send-text.ts — Send a simple text message with typed error handling (ApiError, RateLimitError)
  • media-upload.ts — Upload a file from disk, send as image message, handle MediaError
  • templates.ts — List existing templates, create new template with TemplateBuilder, send template message
  • webhooks-express.ts — Complete Express server with webhook middleware (GET verification, POST event handling)
  • webhooks-nextjs.ts — Next.js App Router webhook handler (app/api/webhook/route.ts structure)
  • phone-numbers.ts — List phone numbers, manage business profile, request/verify verification code
  • multi-account.ts — Manage multiple WABAs with distribution strategies (round-robin, weighted, sticky), broadcast messaging, and dynamic account management

Run any example with:

# Set required environment variables
export WHATSAPP_ACCESS_TOKEN="your_access_token"
export WHATSAPP_PHONE_NUMBER_ID="your_phone_number_id"

# Run with tsx
npx tsx examples/send-text.ts

Error Handling

The SDK uses a typed error hierarchy for precise error handling. All errors extend WhatsAppError for unified catch blocks:

Error Class Hierarchy

WhatsAppError (base)
├── ApiError (API response errors)
│   ├── RateLimitError (429 Too Many Requests)
│   └── AuthenticationError (401 Unauthorized)
├── NotFoundError (semantic "resource missing" — see note below)
├── ValidationError (client-side validation)
├── WebhookVerificationError (signature verification)
└── MediaError (media upload/download)

Note on NotFoundError: It deliberately does not extend ApiError. Meta's API returns 200 with an empty data array for several "missing resource" cases (e.g. getBusinessProfile when the profile is not provisioned). The SDK surfaces those as NotFoundError, which a catch (err) { if (err instanceof ApiError && err.statusCode === 404) } branch will not catch. See the example under "Error Handling Patterns" below for the recommended dual-catch shape.

Error Properties

| Error Class | Extends | Properties | | -------------------------- | --------------- | ---------------------------------------------------------------------------------------------- | | WhatsAppError | Error | code: string | | ApiError | WhatsAppError | statusCode: numbererrorType: stringerrorSubcode?: numberfbTraceId?: string | | RateLimitError | ApiError | All ApiError propertiesretryAfterMs?: number | | AuthenticationError | ApiError | All ApiError properties | | NotFoundError | WhatsAppError | resource?: string | | ValidationError | WhatsAppError | field?: string | | WebhookVerificationError | WhatsAppError | None | | MediaError | WhatsAppError | mediaType?: string |

Error Handling Patterns

1. Handling API errors with status code checks:

import { ApiError, RateLimitError } from '@abdelrahmannasr/wa-cloud-api';

try {
  await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });
} catch (error) {
  if (error instanceof ApiError) {
    console.error(`API Error ${error.statusCode}: ${error.message}`);
    console.error(`Error type: ${error.errorType}`);
    if (error.fbTraceId) {
      console.error(`FB Trace ID: ${error.fbTraceId}`);
    }
  } else {
    console.error('Unexpected error:', error);
  }
}

2. Handling rate limits with retry delay:

import { RateLimitError } from '@abdelrahmannasr/wa-cloud-api';

try {
  await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });
} catch (error) {
  if (error instanceof RateLimitError) {
    const delayMs = error.retryAfterMs || 60000; // Default 60s if not provided
    console.log(`Rate limited. Retry after ${delayMs}ms`);

    // Wait and retry
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });
  } else {
    throw error; // Re-throw if not a rate limit error
  }
}

3. Handling validation errors with field identification:

import { ValidationError } from '@abdelrahmannasr/wa-cloud-api';

try {
  await wa.media.upload({
    file: buffer,
    mimeType: 'invalid/type', // Invalid MIME type
    category: 'document',
    filename: 'file.txt',
  });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`Validation failed: ${error.message}`);
    if (error.field) {
      console.error(`Invalid field: ${error.field}`);
    }
  } else {
    throw error;
  }
}

4. Handling "resource missing" responses separately from 404s:

import { ApiError, NotFoundError } from '@abdelrahmannasr/wa-cloud-api';

try {
  const profile = await wa.phoneNumbers.getBusinessProfile(phoneNumberId);
  console.log(profile.data.description);
} catch (error) {
  if (error instanceof NotFoundError) {
    // Meta returned 200 with an empty data array — the resource simply
    // isn't provisioned. `error.resource` names the specific resource.
    console.log(`No ${error.resource} configured yet`);
  } else if (error instanceof ApiError && error.statusCode === 404) {
    // Explicit wire-level 404 from Meta — e.g. the phoneNumberId is bogus.
    console.error('Unknown phone number ID');
  } else {
    throw error;
  }
}

Advanced Usage

Direct Module Imports

For advanced use cases or tree-shaking, import individual modules via dedicated subpaths:

// Import only what you need
import { Messages } from '@abdelrahmannasr/wa-cloud-api/messages';
import { Media, MEDIA_CONSTRAINTS } from '@abdelrahmannasr/wa-cloud-api/media';
import { Templates, TemplateBuilder } from '@abdelrahmannasr/wa-cloud-api/templates';
import { PhoneNumbers } from '@abdelrahmannasr/wa-cloud-api/phone-numbers';
import { WhatsAppMultiAccount, RoundRobinStrategy } from '@abdelrahmannasr/wa-cloud-api/multi-account';
import { Webhooks, createExpressMiddleware } from '@abdelrahmannasr/wa-cloud-api/webhooks';
import { WhatsAppError, ApiError } from '@abdelrahmannasr/wa-cloud-api/errors';

Available subpath exports:

| Subpath | Primary Exports | |---------|----------------| | @abdelrahmannasr/wa-cloud-api | WhatsApp (unified client), all modules | | @abdelrahmannasr/wa-cloud-api/messages | Messages, all message type interfaces | | @abdelrahmannasr/wa-cloud-api/media | Media, MEDIA_CONSTRAINTS, media types | | @abdelrahmannasr/wa-cloud-api/templates | Templates, TemplateBuilder, validation constants | | @abdelrahmannasr/wa-cloud-api/phone-numbers | PhoneNumbers, business profile types | | @abdelrahmannasr/wa-cloud-api/multi-account | WhatsAppMultiAccount, distribution strategies | | @abdelrahmannasr/wa-cloud-api/webhooks | Webhooks, middleware factories, parser | | @abdelrahmannasr/wa-cloud-api/errors | WhatsAppError, ApiError, RateLimitError, etc. |

All subpaths support ESM (import), CommonJS (require), and include full TypeScript declarations.

// Direct module usage with HttpClient
import { HttpClient } from '@abdelrahmannasr/wa-cloud-api';
import { Messages } from '@abdelrahmannasr/wa-cloud-api/messages';

const client = new HttpClient({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
});

const messages = new Messages(client, process.env.WHATSAPP_PHONE_NUMBER_ID!);

await messages.sendText({
  to: '1234567890',
  body: 'Hello!',
});

client.destroy();

Custom Rate Limiter

const wa = new WhatsApp({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  rateLimitConfig: {
    maxTokens: 1000, // Enterprise tier
    refillRate: 1000,
    enabled: true,
  },
});

Custom Retry Configuration

const wa = new WhatsApp({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  retryConfig: {
    maxRetries: 5,
    baseDelayMs: 2000,
    maxDelayMs: 60000,
    jitterFactor: 0.3,
  },
});

Request Options

Pass custom options to individual requests:

await wa.messages.sendText(
  {
    to: '1234567890',
    body: 'Hello!',
  },
  {
    timeoutMs: 10000, // Override timeout for this request
    skipRateLimit: false,
    skipRetry: false,
  },
);

ESM & CJS

This SDK supports both ESM and CommonJS:

ESM (recommended):

import { WhatsApp } from '@abdelrahmannasr/wa-cloud-api';

CommonJS:

const { WhatsApp } = require('@abdelrahmannasr/wa-cloud-api');

Contributing

Contributions are welcome. Before opening a PR, please read:

  • CONTRIBUTING.md — quick-start, Conventional Commits conventions, development commands, and code style
  • CODE_OF_CONDUCT.md — behavioral expectations for everyone participating in this project

Good first issues and feature requests are tracked on the GitHub issues page.

Security

For security vulnerabilities, please follow the responsible-disclosure process described in SECURITY.md. Do not open a public GitHub issue for security reports.

License

MIT © AbdelRahman Nasr