Kapso
WhatsApp adapter for Chat SDK backed by Kapso webhooks, sends, media, reactions, contacts, conversations, and message history.
Install
pnpm add @kapso/chat-adapter chatInstall a durable Chat SDK state adapter for production. The examples below use @chat-adapter/state-memory for local development.
Quick start
import { Chat } from "chat";
import { createMemoryState } from "@chat-adapter/state-memory";
import { createKapsoAdapter } from "@kapso/chat-adapter";
export const bot = new Chat({
userName: "support",
state: createMemoryState(),
adapters: {
kapso: createKapsoAdapter({
kapsoApiKey: process.env.KAPSO_API_KEY,
phoneNumberId: process.env.KAPSO_PHONE_NUMBER_ID,
webhookSecret: process.env.KAPSO_WEBHOOK_SECRET,
}),
},
});
bot.onDirectMessage(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});Kapso webhooks are direct-message conversations. Replies, cards, files, reactions, and history calls use the same Chat SDK Thread and Message APIs as the official adapters.
Configuration
Prop
Type
Environment variables
| Variable | Required | Description |
|---|---|---|
KAPSO_API_KEY | Yes | Kapso API key used for sends, history, contacts, conversations, and media. |
KAPSO_PHONE_NUMBER_ID | Recommended | WhatsApp phone number ID connected in Kapso. Required for openDM() and useful as a webhook fallback. |
KAPSO_WEBHOOK_SECRET | Recommended | Secret used to verify Kapso X-Webhook-Signature webhook deliveries. |
KAPSO_BASE_URL | No | Kapso proxy URL. Defaults to https://api.kapso.ai/meta/whatsapp. |
KAPSO_BOT_USERNAME | No | Bot display name. Defaults to the Chat SDK userName after initialization. |
Platform setup
- Create or use a Kapso integration and copy your API key.
- Copy the WhatsApp phone number ID connected in Kapso if the bot will initiate outbound direct messages.
- Create a webhook secret in Kapso and set the same value as
KAPSO_WEBHOOK_SECRET. - Configure your webhook endpoint URL to the public route that forwards requests to
bot.webhooks.kapso. - Subscribe to
whatsapp.message.received; addwhatsapp.message.sentonly if your app needs sent-message echoes.
Webhook route
Kapso sends platform webhooks as POST requests. Forward the raw Request to Chat SDK:
import { bot } from "@/lib/bot";
export async function POST(request: Request): Promise<Response> {
return bot.webhooks.kapso(request);
}The adapter verifies Kapso X-Webhook-Signature headers by default and returns 401 for invalid signed requests. For unsigned local fixtures only, pass verifyWebhookSignatures: false.
Sending messages
Reply inside an incoming WhatsApp handler:
bot.onDirectMessage(async (thread, message) => {
await thread.post({
markdown: `Received: **${message.text}**`,
});
});Start a WhatsApp conversation from your app with openDM():
import type { KapsoAdapter } from "@kapso/chat-adapter";
const adapter = bot.getAdapter("kapso") as KapsoAdapter;
const threadId = await adapter.openDM("15551234567");
const thread = bot.thread(threadId);
await thread.post("Hello from Kapso.");Buttons and cards
Chat SDK card buttons render as WhatsApp reply buttons.
import { Actions, Button, Card } from "chat";
await thread.post(
Card({
title: "Approve refund?",
children: [
Actions([
Button({ id: "approve", label: "Approve", value: "refund-123" }),
Button({ id: "reject", label: "Reject", value: "refund-123" }),
]),
],
})
);WhatsApp supports up to 3 reply buttons. Button labels must be 1-20 characters, and the adapter throws a validation error instead of silently truncating labels or dropping buttons.
Media
Send files through Chat SDK:
import { readFile } from "node:fs/promises";
await thread.post({
markdown: "Here is the receipt.",
files: [
{
filename: "receipt.pdf",
mimeType: "application/pdf",
data: await readFile("receipt.pdf"),
},
],
});Inbound media is exposed as Chat SDK attachments. When Kapso includes a mirrored media URL, the attachment has url. When a WhatsApp media ID is available, the attachment has lazy fetchData().
History
With KAPSO_API_KEY, history reads from Kapso:
const page = await thread.adapter.fetchMessages(thread.id, { limit: 20 });fetchThread() enriches metadata with Kapso contact and conversation records when available.
Thread IDs
Current thread IDs are encoded as:
kapso:<base64url(phoneNumberId)>:<base64url(waId)>[:<base64url(conversationId)>]Use helpers instead of constructing thread IDs manually:
const threadId = adapter.encodeThreadId({
phoneNumberId: "16315558151",
waId: "15551234567",
});
const decoded = adapter.decodeThreadId(threadId);Limitations
- Direct Meta webhook setup is a compatibility path; Kapso platform webhooks are preferred.
- Message edit/delete is not supported by WhatsApp/Kapso for recipient devices.
- Raw WhatsApp templates, flows, and catalogs should be sent through
@kapso/whatsapp-cloud-apidirectly alongside the Chat SDK adapter. - Streaming is not supported by WhatsApp/Kapso for recipient devices.
Links
Feature support
Messaging
| Feature | Supported |
|---|---|
| Post message | |
| Edit message | |
| Delete message | |
| File uploads | Images, video, audio, documents, stickers |
| Streaming | |
| Scheduled messages |
Rich content
| Feature | Supported |
|---|---|
| Card format | WhatsApp-compatible text and actions |
| Buttons | Up to 3 reply buttons |
| Link buttons | |
| Select menus | |
| Tables | |
| Fields | |
| Images in cards | Sent as media |
| Modals |
Conversations
| Feature | Supported |
|---|---|
| Slash commands | |
| Mentions | |
| Add reactions | |
| Remove reactions | |
| Typing indicator | |
| DMs | |
| Ephemeral messages | |
| User lookup | |
| Parent subject | |
| Native client | |
| Custom API endpoint |
Message history
| Feature | Supported |
|---|---|
| Fetch messages | |
| Fetch single message | |
| Fetch thread info | |
| Fetch channel messages | |
| List threads | |
| Fetch channel info | |
| Post channel message |