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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@kapso/whatsapp-cloud-api

v0.1.1

Published

TypeScript client for WhatsApp Business Cloud API with typed responses and Zod-validated builders.

Readme

whatsapp-cloud-api-js

npm version npm downloads

TypeScript client for the WhatsApp Cloud API.

Install

npm install @kapso/whatsapp-cloud-api

Quick start

import { WhatsAppClient } from "@kapso/whatsapp-cloud-api";

const client = new WhatsAppClient({
  accessToken: process.env.WHATSAPP_TOKEN!,
  // or route via Kapso proxy:
  // baseUrl: "https://api.kapso.ai/meta/whatsapp",
  // kapsoApiKey: process.env.KAPSO_API_KEY,
});

await client.messages.sendText({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  body: "Hello from Kapso",
});

Choose your setup

  1. Meta setup (~ 1 hour)

Create a Meta WhatsApp app, generate a system token, and link a WhatsApp Business phone number in Meta Business Manager.

  1. Kapso proxy (~ 2 minutes)

Have Kapso provision and connect a WhatsApp number for you, then use your Kapso API key and base URL to begin sending immediately.

Query conversations, messages, contacts, and more.

API surface

Core

Kapso proxy extras

Requires baseUrl and kapsoApiKey.

Using the Kapso Proxy

To use Kapso’s proxy, set the client base URL and API key:

const client = new WhatsAppClient({
  baseUrl: "https://api.kapso.ai/meta/whatsapp",
  kapsoApiKey: process.env.KAPSO_API_KEY!,
});

Why Kapso?

  • Get a WhatsApp API for your number in ~2 minutes.
  • Built‑in inbox for your team.
  • Query conversations, messages and contacts.
  • Automatic backup to Supabase.
  • Webhooks for critical events: message received, message sent, conversation inactive, and more.
  • Get a US phone number for WhatsApp (works globally).
  • Multi‑tenant by design — onboard thousands of customers safely.
  • And more.

Notes:

  • Media GET/DELETE requires phoneNumberId query on the proxy.
  • Responses mirror Meta’s Cloud API message schema.
  • Kapso-only enrichments live under the kapso key; use the fields parameter (for example fields: "kapso(flow_response,flow_token)") to opt into specific fields or fields: "kapso()" to omit them entirely.
  • You can also pass a bearer accessToken instead of kapsoApiKey if you’ve stored a token with Kapso.

Sending messages

Below are concise examples for common message types. Assume client is created as shown above.

Text

await client.messages.sendText({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  body: "Hello!",
});

Image

By media ID:

await client.messages.sendImage({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  image: { id: "<MEDIA_ID>", caption: "Check this out" },
});

By link:

await client.messages.sendImage({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  image: { link: "https://example.com/photo.jpg", caption: "Photo" },
});

Document

await client.messages.sendDocument({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  document: { link: "https://example.com/invoice.pdf", filename: "invoice.pdf", caption: "Invoice" },
});

Video

await client.messages.sendVideo({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  video: { link: "https://example.com/clip.mp4", caption: "Clip" },
});

Sticker

await client.messages.sendSticker({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  sticker: { id: "<MEDIA_ID>" },
});

Location

await client.messages.sendLocation({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  location: { latitude: -33.45, longitude: -70.66, name: "Santiago", address: "CL" },
});

Contacts

await client.messages.sendContacts({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  contacts: [
    { name: { formattedName: "John Doe" }, phones: [{ phone: "+15551234567", type: "WORK" }] },
  ],
});

Reaction

await client.messages.sendReaction({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  reaction: { messageId: "wamid......", emoji: "😀" },
});

Mark read & typing indicator

await client.messages.markRead({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  messageId: "wamid......",
  typingIndicator: { type: "text" },
});

Interactive buttons

await client.messages.sendInteractiveButtons({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  header: { type: "image", image: { id: "<MEDIA_ID>" } },
  bodyText: "Pick an option",
  footerText: "Footer",
  buttons: [ { id: "accept", title: "Accept" }, { id: "decline", title: "Decline" } ],
});

Interactive CTA URL

await client.messages.sendInteractiveCtaUrl({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  header: { type: "image", image: { link: "https://example.com/banner.png" } },
  bodyText: "Tap the button to see dates.",
  parameters: { displayText: "See Dates", url: "https://example.com?utm=wa" }
});

Catalog Message

await client.messages.sendInteractiveCatalogMessage({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  bodyText: "Browse our catalog on WhatsApp",
  parameters: { thumbnailProductRetailerId: "SKU-123" }
});

Flows

Use client.flows.deploy() for idempotent deployments, or create/updateAsset/publish/preview for granular control. Server utilities (receiveFlowEvent, respondToFlow, downloadFlowMedia) handle Data Endpoint callbacks.

Deploy a Flow

import { WhatsAppClient } from "@kapso/whatsapp-cloud-api";

const flowJson = {
  version: "7.2",
  screens: [
    {
      id: "CSAT",
      terminal: true,
      layout: {
        type: "SingleColumnLayout",
        children: [
          { type: "RadioButtonsGroup", name: "rating", label: "Rate us", dataSource: [
            { id: "up", title: "👍" },
            { id: "down", title: "👎" }
          ] },
          { type: "Footer", label: "Submit", onClickAction: { name: "complete", payload: { rating: "${form.rating}" } } }
        ]
      }
    }
  ]
};

const client = new WhatsAppClient({ accessToken: process.env.WHATSAPP_TOKEN! });

await client.flows.deploy(flowJson, {
  wabaId: process.env.WABA_ID!,
  name: "csat-flow",
  publish: true,
  preview: true
});

Send a Flow message

import { WhatsAppClient, type FlowInteractiveInput } from "@kapso/whatsapp-cloud-api";

const client = new WhatsAppClient({ accessToken: process.env.WHATSAPP_TOKEN! });

await client.messages.sendInteractiveFlow({
  phoneNumberId: "1234567890",
  to: "+15551234567",
  bodyText: "Check out our new experience",
  parameters: {
    flowId: "1234567890",
    flowCta: "Open",
    flowToken: "token123",
    flowAction: "navigate",
    flowActionPayload: { screen: "WELCOME" }
  }
});

flowCta is required by Meta. flowMessageVersion defaults to "3" when omitted.

For a full walkthrough (authoring guidance, deployment scripts, Express/Edge examples, and manual testing tips) see docs/flows.md.

Templates

Build with components

Use buildTemplatePayload as the primary way to build templates. It accepts Meta‑style components, normalizes casing, and enforces shape (e.g., language.policy = 'deterministic' when using an object).

import { buildTemplatePayload } from '@kapso/whatsapp-cloud-api';

const template = buildTemplatePayload({
  name: 'order_confirmation',
  language: 'en_US', // or { code: 'en_US', policy: 'deterministic' }
  components: [
    { type: 'body', parameters: [{ type: 'text', text: 'Jessica', parameter_name: 'customer_name' }] },
  ],
});

When you pass raw Meta-style components, keep the snake_case field (parameter_name) Meta expects.

Typed builder

Prefer typed guardrails? Use buildTemplateSendPayload. It outputs the same Meta structure but gives compile‑time guidance. Example with body parameters and a Flow button:

import { buildTemplateSendPayload } from '@kapso/whatsapp-cloud-api';

const template = buildTemplateSendPayload({
  name: 'order_confirmation',
  language: 'en_US',
  body: [
    { type: 'text', text: 'Jessica', parameterName: 'customerName' },
    { type: 'text', text: 'SKBUP2-4CPIG9', parameterName: 'orderId' },
  ],
  buttons: [
    {
      type: 'button',
      subType: 'flow',
      index: 0,
      parameters: [{ type: 'action', action: { flow_token: 'FT_123', flow_action_data: { step: 'one' } } }],
    },
  ],
});

The typed builder accepts camelCase parameterName and the client automatically snake-cases it when sending.

Template creation

The creation builder validates components and examples like Meta’s review.

Minimal examples:

import { buildTemplateDefinition } from '@kapso/whatsapp-cloud-api';

// Authentication (copy code)
const authenticationTemplate = buildTemplateDefinition({
  name: 'authentication_code',
  language: 'en_US',
  category: 'AUTHENTICATION',
  messageSendTtlSeconds: 60,
  components: [
    { type: 'BODY', addSecurityRecommendation: true },
    { type: 'FOOTER', codeExpirationMinutes: 10 },
    { type: 'BUTTONS', buttons: [{ type: 'OTP', otpType: 'COPY_CODE' }] },
  ],
});

// Named parameters (parameter_format = NAMED)
const namedOrderTemplate = buildTemplateDefinition({
  name: 'order_confirmation_named',
  language: 'en_US',
  category: 'UTILITY',
  parameterFormat: 'NAMED',
  components: [
    {
      type: 'BODY',
      text: 'Thank you, {{customer_name}}! Your order {{order_number}} ships {{ship_date}}.',
      example: {
        bodyTextNamedParams: [
          { paramName: 'customer_name', example: 'Pablo' },
          { paramName: 'order_number', example: '860198-230332' },
          { paramName: 'ship_date', example: '2025-11-15' },
        ],
      },
    },
  ],
});

// Limited-time offer
const limitedTimeOfferTemplate = buildTemplateDefinition({
  name: 'limited_offer', language: 'en_US', category: 'MARKETING',
  components: [
    { type: 'BODY', text: 'Hello {{1}}', example: { bodyText: [['Pablo']] } },
    { type: 'LIMITED_TIME_OFFER', limitedTimeOffer: { text: 'Expiring!', hasExpiration: true } },
  ],
});

// Catalog / MPM / SPM
const catalogTemplate = buildTemplateDefinition({
  name: 'catalog_push', language: 'en_US', category: 'MARKETING',
  components: [ { type: 'BODY', text: 'Browse our catalog' }, { type: 'BUTTONS', buttons: [{ type: 'CATALOG', text: 'View catalog' }] } ],
});

parameterFormat matches the API’s parameter_format field. When set to "NAMED", use named placeholders (for example {{customer_name}}) and provide examples via bodyTextNamedParams / headerTextNamedParams so WhatsApp can validate your template.

Query history & contacts

When you point the client to Kapso’s proxy (baseUrl: "https://api.kapso.ai/meta/whatsapp" plus kapsoApiKey), you can query stored data in addition to sending messages.

const client = new WhatsAppClient({
  baseUrl: "https://api.kapso.ai/meta/whatsapp",
  kapsoApiKey: process.env.KAPSO_API_KEY!,
});

// Conversations
const conversations = await client.conversations.list({
  phoneNumberId: "647015955153740",
  status: "active",
  limit: 50,
});

const conversation = await client.conversations.get({ conversationId: conversations.data[0].id, });
await client.conversations.updateStatus({ conversationId: conversation.id, status: "ended", });

// Message history
const history = await client.messages.query({
  phoneNumberId: "647015955153740",
  direction: "inbound",
  since: "2025-01-01T00:00:00Z",
  limit: 50,
  after: conversations.paging.cursors.after,
});

// Contacts
const contacts = await client.contacts.list({ phoneNumberId: "647015955153740", customerId: "123", });
await client.contacts.update({
  phoneNumberId: "647015955153740",
  waId: contacts.data[0].waId,
  metadata: { tags: ["vip"], source: "import" },
});

// Call logs
const calls = await client.calls.list({ phoneNumberId: "647015955153740", direction: "INBOUND", limit: 20, });
const call = await client.calls.get({ phoneNumberId: "647015955153740", callId: calls.data[0].id, });

All history endpoints return Meta-compatible records with Graph paging:

  • page.data (camelCased) mirrors Meta’s message/contact/conversation/call schema.
  • page.paging exposes cursors.before / cursors.after plus next / previous URLs when present.
  • Supply fields: buildKapsoFields() (or the string "kapso(default)") to include all Kapso extensions, or pass your own subset such as fields: "kapso(flow_response,flow_token)". Use fields: "kapso()" to omit Kapso extras entirely.
  • When you store messages via Kapso, request kapso(content) to hydrate the normalized message with the original payload fragment (for example, catalog interactive content).
  • Tip: buildKapsoFields is exported from the SDK, so you can import { buildKapsoFields } from "@kapso/whatsapp-cloud-api"; and drop it straight into your queries.

Templates

Create

import { TemplateDefinition } from "@kapso/whatsapp-cloud-api";

const templateDefinition = TemplateDefinition.buildTemplateDefinition({
  name: "seasonal_promo",
  language: "en_US",
  category: "MARKETING",
  parameterFormat: "NAMED",
  components: [
    {
      type: "HEADER",
      format: "TEXT",
      text: "Our {{sale_name}} is on!",
      example: { headerTextNamedParams: [{ paramName: "sale_name", example: "Summer Sale" }] }
    },
    {
      type: "BODY",
      text: "Shop now through {{end_date}} using code {{discount_code}}",
      example: {
        bodyTextNamedParams: [
          { paramName: "end_date", example: "Aug 31" },
          { paramName: "discount_code", example: "SALE25" }
        ]
      }
    },
    { type: "FOOTER", text: "Tap a button below" },
    {
      type: "BUTTONS",
      buttons: [
        { type: "QUICK_REPLY", text: "Unsubscribe" },
        {
          type: "URL",
          text: "Shop",
          url: "https://store.example/promo?code={{discount_code}}",
          example: ["SALE25"]
        }
      ]
    }
  ],
});

await client.templates.create({
  businessAccountId: "<WABA_ID>",
  name: templateDefinition.name,
  language: templateDefinition.language,
  category: templateDefinition.category,
  parameterFormat: templateDefinition.parameterFormat,
  components: templateDefinition.components,
});

Send a template

import { buildTemplateSendPayload } from "@kapso/whatsapp-cloud-api";

const templatePayload = buildTemplateSendPayload({
  name: "seasonal_promo",
  language: "en_US",
  header: { type: "image", image: { link: "https://cdn.example/banner.jpg" } },
  body: [ { type: "text", text: "Aug 31" }, { type: "text", text: "SALE25" } ],
  buttons: [ { type: "button", subType: "quick_reply", index: 0, parameters: [{ type: "payload", payload: "STOP" }] } ],
});

await client.messages.sendTemplate({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  template: templatePayload,
});

Media

const imageBlob = new Blob([/* binary data */], { type: "image/png" });
await client.media.upload({ phoneNumberId: "<PHONE_NUMBER_ID>", type: "image", file: imageBlob, fileName: "photo.png", });
const metadata = await client.media.get({ mediaId: "<MEDIA_ID>", phoneNumberId: "<PHONE_NUMBER_ID>", }); // Kapso requires phoneNumberId
await client.media.delete({ mediaId: "<MEDIA_ID>", phoneNumberId: "<PHONE_NUMBER_ID>", });

Receiving media

Common cases:

  1. URL‑first with Kapso

Kapso stores inbound media and now also mirrors outbound media shortly after send. Ask for kapso(media_url) when listing messages and render the URL directly (SSR‑friendly).

import { buildKapsoMessageFields } from "@kapso/whatsapp-cloud-api";

const fields = buildKapsoMessageFields("media_url");
const page = await client.messages.listByConversation({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  conversationId: "<CONVERSATION_ID>",
  fields,
});

const msg = page.data.find(m => m.type === "image");
const src = msg?.kapso?.mediaUrl ?? msg?.image?.link; // use direct URL when present
  1. Bytes fallback (universal)

If you need the raw bytes or the URL has not been mirrored yet, use download(). The SDK automatically skips auth headers for public WhatsApp CDNs and uses them for Kapso hosts.

Key points:

  • client.media.download({ mediaId, ... }) resolves the short‑lived URL via media.get() then fetches the bytes.
  • Return types: default ArrayBuffer, as: "blob"Blob, as: "response"Response.
  • Direct Meta: phoneNumberId is not required.
  • Kapso proxy: pass phoneNumberId.

Examples:

// 1) From a message record you loaded (e.g., via client.messages.query):
const { data } = await client.messages.query({ phoneNumberId: "<PHONE_NUMBER_ID>", limit: 1, });
const msg = data[0];

if (msg.type === "image" && msg.image?.id) {
  const mediaId = msg.image.id;
  const bytes = await client.media.download({ mediaId, phoneNumberId: "<PHONE_NUMBER_ID>", });
  // bytes is an ArrayBuffer; do what you need with it
}

Phone numbers

await client.phoneNumbers.requestCode({ phoneNumberId: "<PHONE_NUMBER_ID>", codeMethod: "SMS", language: "en_US", });
await client.phoneNumbers.verifyCode({ phoneNumberId: "<PHONE_NUMBER_ID>", code: "123456", });
await client.phoneNumbers.register({ phoneNumberId: "<PHONE_NUMBER_ID>", pin: "000111", });
await client.phoneNumbers.settings.update({ phoneNumberId: "<PHONE_NUMBER_ID>", fallbackLanguage: "en_US", });
await client.phoneNumbers.businessProfile.update({ phoneNumberId: "<PHONE_NUMBER_ID>", about: "My Shop", websites: ["https://example.com"], });

Webhooks

import express from "express";
import { normalizeWebhook, verifySignature } from "@kapso/whatsapp-cloud-api/server";

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifySignature({
    appSecret: process.env.META_APP_SECRET!,
    rawBody: req.body,
    signatureHeader: req.headers["x-hub-signature-256"] as string,
  });
  if (!ok) return res.status(401).end();

  const payload = JSON.parse(req.body.toString("utf8"));
  const events = normalizeWebhook(payload);

  events.messages.forEach((message) => {
    // message matches the same shape returned by client.messages.query()
  });

  events.statuses.forEach((status) => {
    // handle delivery receipts
  });

  events.calls.forEach((call) => {
    // handle calling events
  });

  res.sendStatus(200);
});

// events.contacts contains the contact array from the webhook, already camelCased

normalizeWebhook() unwraps the raw Graph payload, returning { messages, statuses, calls, contacts } with camelCased fields so webhook events and history queries share the same Meta-compatible structure. Each normalized message also gets kapso.direction ("inbound"/"outbound") and SMB echoes are tagged with kapso.source = "smb_message_echo" so you can tell when the business initiated a message. All other webhook field payloads are exposed under events.raw.<fieldName> (camelCased), so you can react to updates like accountAlerts, templateCategoryUpdate, etc., without additional parsing.

Raw fetch helper

Use client.fetch(url, init?) to make a request to any absolute URL with the client’s auth headers applied. Most users do not need this for media anymore because media.download() handles header policy automatically.

// Sends Authorization (Meta) or X-API-Key (Kapso) automatically
const response = await client.fetch("https://files.example/resource", { headers: { Accept: "image/*" }, });

Typed responses

  • All helpers return typed payloads (e.g., SendMessageResponse, MediaUploadResponse, etc.).
  • You can also call the low-level client with typing:
const response = await client.request<MyType>("GET", "<path>", { responseType: "json", });

Error handling

When a response is not OK, the client throws an Error whose message includes the HTTP status and response text, e.g.:

Meta API request failed with status 400: {"error":{...}}

License

MIT