WhatsApp adapter for Chat SDK via Kapso, using Kapso webhook payloads and history APIs.
Unofficial community-maintained package. This adapter is not published or maintained by Kapso. If Kapso later ships an official package, prefer the official package name for new integrations.
pnpm add @luicho/kapso-chat-sdk chat @chat-adapter/state-memoryimport { Chat } from "chat";
import { MemoryStateAdapter } from "@chat-adapter/state-memory";
import { createKapsoAdapter } from "@luicho/kapso-chat-sdk";
export const bot = new Chat({
userName: "My Bot",
adapters: {
kapso: createKapsoAdapter(),
},
state: new MemoryStateAdapter(),
});
bot.onDirectMessage(async (thread, message) => {
await thread.subscribe();
await thread.post(`You said: ${message.text}`);
});When using createKapsoAdapter() without arguments, credentials are auto-detected from environment variables. The example above uses in-memory state for local development; for deployed workloads, use a durable Chat SDK state adapter.
WhatsApp conversations via Kapso are always 1:1 DMs. onDirectMessage is usually the clearest entry point. If you do not register any onDirectMessage handlers, DM messages fall through to onNewMention for backward compatibility. See the Chat SDK adapters docs and direct messages guide for broader integration patterns.
The repo includes standalone examples under examples/. Each example has its own package.json, server.ts, and .env.example.
examples/basicshows the smallest echo-bot setup.examples/support-botshows a simple ticket-style flow with thread state.examples/interactive-menushows WhatsApp interactive reply buttons via Chat SDK cards.examples/mediashows how inbound media attachments are exposed and downloaded.examples/ai-replyshows a streamed OpenAI-powered reply bot using AI SDK 6.
To run an example, open the example directory, install its dependencies with your preferred package manager, copy .env.example to .env, fill in the required credentials, and run its dev script.
| Variable | Required | Example | Description |
|---|---|---|---|
KAPSO_API_KEY |
Yes | kap_live_abc123 |
Kapso project API key used for outbound sends, history queries, and thread enrichment. |
KAPSO_PHONE_NUMBER_ID |
Yes | 123456789 |
WhatsApp phone number ID connected in Kapso. Used for sends, history lookups, and thread IDs. |
KAPSO_WEBHOOK_SECRET |
Yes | whsec_abc123 |
Shared secret used to verify the X-Webhook-Signature header on Kapso webhook deliveries. |
KAPSO_BASE_URL |
No | https://api.kapso.ai/meta/whatsapp |
Override the Kapso proxy base URL. Leave unset unless Kapso tells you to use a different base URL. |
KAPSO_BOT_USERNAME |
No | support-bot |
Override the bot display name used by Chat SDK for bot-authored messages. Defaults to kapso-bot. |
All configuration can be provided explicitly or via environment variables.
| Option | Type | Default | Description |
|---|---|---|---|
kapsoApiKey |
string |
KAPSO_API_KEY |
Kapso API key used by the Kapso proxy client. |
phoneNumberId |
string |
KAPSO_PHONE_NUMBER_ID |
WhatsApp phone number ID that owns the conversation. |
webhookSecret |
string |
KAPSO_WEBHOOK_SECRET |
Secret used to verify Kapso webhook signatures. |
baseUrl |
string |
https://api.kapso.ai/meta/whatsapp |
Kapso proxy base URL for outbound messaging and query APIs. |
userName |
string |
KAPSO_BOT_USERNAME or kapso-bot |
Bot display name used by Chat SDK for self-message display. |
- Open the Kapso Dashboard.
- Click
Add number. - On
Connect number to WhatsApp, chooseInstant setup with US digital numberif you want the fastest Kapso-managed path. - Finish the setup flow for the new number.
- Go to
Phone numbers -> Connected numbers, open the connected number, and copy itsPhone Number ID. Use that value asKAPSO_PHONE_NUMBER_ID. - Go to
API keys, clickCreate API Key, and copy the API key. Set it asKAPSO_API_KEY. - Generate a strong random secret, set it as
KAPSO_WEBHOOK_SECRET, and use the same value as the webhooksecret_key.
Add a public HTTPS POST webhook route that forwards the raw Request to Chat SDK and returns quickly:
import { bot } from "@/lib/bot";
export async function POST(request: Request) {
return bot.webhooks.kapso(request);
}Expose that route at a public HTTPS URL such as https://your-app.com/webhooks/whatsapp.
You can create the webhook in either of these ways.
- Go to
Webhooks. - Stay on the
WhatsApp webhookstab. - Expand the connected number you want to use.
- Click
Add Webhook. - Set
Endpoint URLto your public HTTPS route, for examplehttps://your-app.com/webhooks/whatsapp. - In
Advanced settings, set the webhooksecret_keyto the same value you stored inKAPSO_WEBHOOK_SECRET. - In
Events, enableMessage receivedandMessage sent.
Create the same webhook via the Kapso API:
curl --request POST \
--url "https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/$KAPSO_PHONE_NUMBER_ID/webhooks" \
--header "Content-Type: application/json" \
--header "X-API-Key: $KAPSO_API_KEY" \
--data '{
"whatsapp_webhook": {
"url": "https://your-app.com/webhooks/whatsapp",
"events": ["whatsapp.message.received", "whatsapp.message.sent"],
"secret_key": "your-webhook-secret"
}
}'- For local testing, expose your app with an HTTPS tunnel such as ngrok or Cloudflare Tunnel, then register that URL in the webhook configuration.
- If you enable buffering for
whatsapp.message.received, the adapter handles both immediate single-event deliveries and buffered batched payloads automatically. - Kapso expects your endpoint to return
200 OKwithin 10 seconds. The adapter already verifiesX-Webhook-Signatureand handlesX-Idempotency-Keydedupe hooks for Kapso deliveries.
Relevant Kapso docs:
- Webhook overview
- Webhook security
- TypeScript SDK introduction
- Messages API
- Conversations API
- Contacts API
| Feature | Supported | Notes |
|---|---|---|
| Outbound text messages | Yes | postMessage() sends plain text through Kapso and automatically splits messages over 4096 characters at paragraph or line boundaries when possible. |
| Buffered streaming | Yes | stream() buffers string and markdown_text chunks, ignores non-text stream chunks, and sends one final message through postMessage(). It does not attempt incremental edits because WhatsApp does not support them. |
| Outbound cards | Limited | Cards with up to 3 action buttons are sent as WhatsApp interactive reply buttons. Button titles are truncated to 20 characters, and unsupported cards fall back to text. |
| Reactions | Yes | addReaction() and removeReaction() send WhatsApp reactions through Kapso. Removing a reaction sends an empty emoji string. |
| Mark messages as read | Yes | markAsRead() delegates to the Kapso SDK messages.markRead() helper. |
| Message edit | No | Platform limitation. editMessage() throws because this integration does not support editing previously sent messages. |
| Message delete | No | Platform limitation. deleteMessage() throws because this integration does not support deleting previously sent messages. |
| Attachments, files, and other richer outbound message types | No | Adapter limitation. Outbound attachments, files, media sends, templates, and other richer outbound message types are not implemented in this adapter yet. |
| Typing indicator | No | No standalone adapter-level typing API is exposed here. startTyping() is an intentional parity no-op instead of guessing. |
| Feature | Supported | Notes |
|---|---|---|
whatsapp.message.received handling |
Yes | handleWebhook() verifies X-Webhook-Signature, accepts POST requests only, and processes Kapso webhook payloads into Chat SDK messages. |
| Buffered Kapso deliveries | Yes | Batched deliveries with batch: true and X-Webhook-Batch: true are expanded and processed one message at a time. |
| Inbound text messages | Yes | Text bodies are surfaced as Chat SDK message text. |
| BSUID-based inbound identity | Yes | When Kapso includes business_scoped_user_id, the adapter treats it as the primary thread identity and preserves parent_business_scoped_user_id and username in message.raw.identity. |
| Inbound media messages | Yes | Supported Kapso media payloads are converted into Chat SDK attachments plus readable fallback text. Image, document, audio, video, and sticker attachments also expose lazy attachment.fetchData() download support when Kapso includes a media ID. |
| Inbound reaction messages | Yes | Live webhook reactions call chat.processReaction(...), so bot.onReaction(...) fires. Empty reaction.emoji values are treated as reaction removal. |
| Inbound interactive replies and button callbacks | Yes | Interactive button replies, list replies, and legacy button callbacks call chat.processAction(...), so bot.onAction(...) fires for action payloads instead of treating them as plain messages. |
| WhatsApp Flow completion replies | Yes | Flow completion webhooks (interactive.type === "nfm_reply") are surfaced as normal inbound messages, so handlers like bot.onDirectMessage(...) and bot.onSubscribedMessage(...) can read parsed flow data from message.raw.message.kapso.flow_response. |
| Other Kapso webhook events | Ignored | The adapter acknowledges unsupported event types with 200 OK, but only whatsapp.message.received is processed. |
| Feature | Supported | Notes |
|---|---|---|
| Message history fetching | Yes | fetchMessages() reads stored conversation history from Kapso and returns Chat SDK messages in chronological order. For BSUID-only threads, reload is best effort and depends on Kapso history lookup returning a matching conversation or phone-based identity. |
| Historical reaction events | Limited | Reactions returned by fetchMessages() are still surfaced as fallback messages like [Reaction: 👍]; only live webhooks emit Chat SDK reaction events. |
| Thread enrichment | Yes | fetchThread() enriches metadata with Kapso conversation and contact records when available. BSUID-only thread enrichment is best effort for the current Kapso API surface. |
| DMs | Yes | All conversations are 1:1 DMs. isDM() always returns true. |
kapso:{phoneNumberId}:{identitySegment}
Example:
kapso:123456789:wa.15551234567
kapso:123456789:bs.VVMuMTM0OTEyMDg2NTUzMDI3NDE5MTg
phoneNumberId is the receiving business phone number ID in Kapso.
identitySegment is one of:
wa.<digits>for phone-based WhatsApp identitybs.<base64url(bsuid)>for business-scoped WhatsApp identity
Legacy thread IDs in the old kapso:{phoneNumberId}:{userWaId} format still decode correctly for backward compatibility.
- Confirm
KAPSO_WEBHOOK_SECRETexactly matches the webhooksecret_keyconfigured in Kapso. - Make sure your route passes the raw
Requesttobot.webhooks.kapsoso signature verification uses the original request body.
- Confirm the webhook is registered for the same
KAPSO_PHONE_NUMBER_IDyour adapter uses. - Confirm the webhook is subscribed to
whatsapp.message.received. - Make sure Kapso can reach your route over HTTPS and that the route returns
200 OKquickly.
- This adapter still sends outbound messages by phone-based WhatsApp ID because Kapso does not yet document BSUID-targeted sends.
- If an inbound thread is BSUID-only and the adapter cannot resolve a phone-based target from webhook context or history,
postMessage()throws an explicit error instead of guessing.
- This is expected when Kapso buffering is enabled for
whatsapp.message.received. - The adapter already supports batched bodies with
batch: trueanddata: [...]; no code change is required.
- History and enrichment depend on Kapso having matching conversation and contact records for the same phone number and WhatsApp user.
- If no Kapso conversation is found,
fetchMessages()returns an empty list. - If no Kapso contact or conversation is found,
fetchThread()falls back to the decoded thread ID identity. - For BSUID-only threads, history and metadata reload are best effort until Kapso publishes broader BSUID-first read contracts across all relevant APIs.
- Supported cards are sent as WhatsApp reply buttons when they fit the platform limits.
- Unsupported cards fall back to readable text automatically.
- Attachments, files, and other richer outbound message types still reject. If you need them today, use
@kapso/whatsapp-cloud-apidirectly alongside Chat SDK.
MIT