
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
@kapso/whatsapp-cloud-api
Advanced tools
TypeScript client for WhatsApp Business Cloud API with typed responses and Zod-validated builders.
whatsapp-cloud-api-jsTypeScript client for the WhatsApp Cloud API.
npm install @kapso/whatsapp-cloud-api
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",
});
Create a Meta WhatsApp app, generate a system token, and link a WhatsApp Business phone number in Meta Business Manager.
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.
client.messages — send text/media/interactive/templates, send raw payloads, and mark messages as readclient.templates — list/get/create/delete templates on your WABAclient.media — upload media, fetch metadata, delete mediaclient.phoneNumbers — request/verify code, register/deregister, settings, business profileclient.flows — author, validate, deploy, and preview WhatsApp FlowsverifySignature — verify webhook signatures (app secret)receiveFlowEvent, respondToFlow, downloadFlowMedia — decrypt and respond to Flow callbacksTemplateDefinition — strict template creation buildersbuildTemplateSendPayload — build send-time template payloadsbuildTemplatePayload — accept Meta-style raw components and normalize/camelize inputsRequires baseUrl and kapsoApiKey.
client.conversations — list/get/update conversations across your projectclient.messages.query / listByConversation / get — pull stored message historyclient.contacts — list/get/update contacts, with customerId filterclient.calls — initiate calls plus historic call logs (list/get) and permission helpersKapso Extensions — opt-in to extra fields via fields=kapso(...)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!,
});
Notes:
phoneNumberId query on the proxy.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.accessToken instead of kapsoApiKey if you’ve stored a token with Kapso.Below are concise examples for common message types. Assume client is created as shown above.
await client.messages.sendText({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
body: "Hello!",
});
await client.messages.sendRaw({
phoneNumberId: "<PHONE_NUMBER_ID>",
payload: {
messaging_product: "whatsapp",
recipient_type: "individual",
to: "+15551234567",
type: "text",
text: { body: "Hello from a raw payload" },
},
});
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" },
});
await client.messages.sendDocument({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
document: { link: "https://example.com/invoice.pdf", filename: "invoice.pdf", caption: "Invoice" },
});
await client.messages.sendVideo({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
video: { link: "https://example.com/clip.mp4", caption: "Clip" },
});
await client.messages.sendSticker({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
sticker: { id: "<MEDIA_ID>" },
});
await client.messages.sendLocation({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
location: { latitude: -33.45, longitude: -70.66, name: "Santiago", address: "CL" },
});
await client.messages.sendContacts({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
contacts: [
{ name: { formattedName: "John Doe" }, phones: [{ phone: "+15551234567", type: "WORK" }] },
],
});
await client.messages.sendReaction({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
reaction: { messageId: "wamid......", emoji: "😀" },
});
await client.messages.markRead({
phoneNumberId: "<PHONE_NUMBER_ID>",
messageId: "wamid......",
typingIndicator: { type: "text" },
});
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" } ],
});
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" }
});
await client.messages.sendInteractiveCatalogMessage({
phoneNumberId: "<PHONE_NUMBER_ID>",
to: "+15551234567",
bodyText: "Browse our catalog on WhatsApp",
parameters: { thumbnailProductRetailerId: "SKU-123" }
});
Use client.flows.deploy() for idempotent deployments, or create/updateAsset/publish/preview for granular control. Server utilities (receiveFlowEvent, respondToFlow, downloadFlowMedia) handle Data Endpoint callbacks.
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
});
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" }
}
});
flowCtais required by Meta.flowMessageVersiondefaults to"3"when omitted.
For a full walkthrough (authoring guidance, deployment scripts, Express/Edge examples, and manual testing tips) see docs/flows.md.
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.
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.
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.
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,
});
const message = await client.messages.get({
phoneNumberId: "647015955153740",
messageId: history.data[0].id,
fields: "kapso(default)",
});
// 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.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.kapso(content) to hydrate the normalized message with the original payload fragment (for example, catalog interactive content).buildKapsoFields is exported from the SDK, so you can import { buildKapsoFields } from "@kapso/whatsapp-cloud-api"; and drop it straight into your queries.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,
});
const template = await client.templates.get({
businessAccountId: "<WABA_ID>",
templateId: "<TEMPLATE_ID>",
});
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,
});
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>", });
Common cases:
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
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.ArrayBuffer, as: "blob" → Blob, as: "response" → Response.phoneNumberId is not required.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
}
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"], });
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.
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/*" }, });
SendMessageResponse, MediaUploadResponse, etc.).const response = await client.request<MyType>("GET", "<path>", { responseType: "json", });
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":{...}}
MIT
FAQs
TypeScript client for WhatsApp Business Cloud API with typed responses and Zod-validated builders.
The npm package @kapso/whatsapp-cloud-api receives a total of 9,740 weekly downloads. As such, @kapso/whatsapp-cloud-api popularity was classified as popular.
We found that @kapso/whatsapp-cloud-api demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.