Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f85f2b7
feat: add WhatsApp Business Cloud API adapter
ghellach Feb 25, 2026
28e3365
feat: add media download, attachments, and location support to WhatsA…
ghellach Feb 25, 2026
2669f10
fix: address PR review feedback for WhatsApp adapter
ghellach Feb 25, 2026
b1d781f
Migrate improvements from #179
haydenbleasel Mar 6, 2026
b11625b
fix(whatsapp): add error handling for inbound message processing
haydenbleasel Mar 6, 2026
2300a84
fix(whatsapp): prevent markdown regex from matching across newlines
haydenbleasel Mar 6, 2026
46375cf
fix(whatsapp): use WhatsAppInteractiveMessage type instead of object
haydenbleasel Mar 6, 2026
34ee565
fix(whatsapp): hoist emoji mapping to module-level constant
haydenbleasel Mar 6, 2026
1d022cd
fix(whatsapp): remove duplicate JSDoc comment in types
haydenbleasel Mar 6, 2026
8fccba7
fix(example): add startTyping to WhatsApp recording methods
haydenbleasel Mar 6, 2026
6fcc74f
fix(whatsapp): fix formatting and add package to readme test allowlist
haydenbleasel Mar 6, 2026
4eb3157
fix(whatsapp): use defaultEmojiResolver instead of custom emoji map
haydenbleasel Mar 6, 2026
e020253
fix(whatsapp): make Graph API version configurable
haydenbleasel Mar 7, 2026
f8b4e98
fix(whatsapp): validate lat/lng before constructing Google Maps URL
haydenbleasel Mar 7, 2026
8a66a74
fix(whatsapp): throw on editMessage instead of silently sending new m…
haydenbleasel Mar 7, 2026
8f00a87
fix(whatsapp): document regex asymmetry between toWhatsApp and fromWh…
haydenbleasel Mar 7, 2026
c132f61
fix(whatsapp): document callback data passthrough behavior
haydenbleasel Mar 7, 2026
08eeaff
fix(whatsapp): add editMessage and deleteMessage to recording methods
haydenbleasel Mar 7, 2026
18a6983
fix(whatsapp): preserve escaped formatting chars in toWhatsAppFormat
haydenbleasel Mar 7, 2026
35e5d62
fix(whatsapp): split long messages instead of truncating
haydenbleasel Mar 7, 2026
31514f8
fix(whatsapp): align editMessage/deleteMessage behavior and docs
haydenbleasel Mar 7, 2026
f6e7a93
docs(whatsapp): add adapter documentation page
haydenbleasel Mar 7, 2026
645d022
fix: add whatsapp adapter debug logging and try/catch
cramforce Mar 8, 2026
cb1aac2
fix(whatsapp): convert emoji placeholders in outgoing messages
cramforce Mar 8, 2026
28ff40b
Fix button rendering and streaming (needs to buffer)
cramforce Mar 8, 2026
20574fd
fix(example): handle editMessage failure on WhatsApp
cramforce Mar 8, 2026
37133a3
feat(chat): add onDirectMessage handler, stop treating DMs as mentions
haydenbleasel Mar 8, 2026
e7165f2
feat(chat): always route DMs to onDirectMessage regardless of subscri…
cramforce Mar 8, 2026
661fe29
feat(chat): pass channel as third argument to DirectMessageHandler
cramforce Mar 8, 2026
f758e5d
fix(example): reply to channel instead of thread in DM handler
cramforce Mar 8, 2026
e4c9c03
fix(example): use thread instead of channel for DM operations
cramforce Mar 8, 2026
445df61
Revert "fix(example): use thread instead of channel for DM operations"
cramforce Mar 8, 2026
56e436d
fix(adapters): return valid thread IDs from channelIdFromThreadId
cramforce Mar 8, 2026
20d3716
fix(chat): normalize fullStream in Channel.post() to extract text deltas
cramforce Mar 8, 2026
5b2a3c8
fix(example): use thread.allMessages for DM history instead of adapte…
cramforce Mar 8, 2026
9f029dc
feat(chat): add message history support to Channel for DM platforms
cramforce Mar 8, 2026
f81e336
fix(whatsapp): improve markdown rendering and remove broken typing in…
cramforce Mar 8, 2026
c4b0209
fix(example): reverse channel.messages to chronological order for AI
cramforce Mar 8, 2026
c551d30
fix(chat): auto-sort messages chronologically in toAiMessages
cramforce Mar 8, 2026
5de587a
fix(chat): pass accumulated stream text as markdown in Channel.post()
cramforce Mar 8, 2026
c393ac5
fix(whatsapp): use stringifier options for emphasis and bullets
cramforce Mar 8, 2026
47c69fc
fix(whatsapp): flatten bold inside headings to avoid triple asterisks
cramforce Mar 8, 2026
3195b45
test(whatsapp): add full toBe assertion for complex markdown conversion
cramforce Mar 8, 2026
6169001
test: add WhatsApp replay tests from production recordings
cramforce Mar 8, 2026
fdd763b
Merge main into feat/adapter-whatsapp
ghellach Mar 8, 2026
71d543b
fix(whatsapp): fix type narrowing in replay test after merge
ghellach Mar 8, 2026
9a06d4f
Merge origin/main into feat/adapter-whatsapp
haydenbleasel Mar 10, 2026
e5bd68f
Update WhatsApp logo
haydenbleasel Mar 10, 2026
24359b1
Update adapters.json
haydenbleasel Mar 10, 2026
9320d30
Update logos.tsx
haydenbleasel Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-whatsapp-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chat-adapter/whatsapp": minor
---

Add WhatsApp adapter using Meta's WhatsApp Business Cloud API
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ This is a **pnpm monorepo** using **Turborepo** for build orchestration. All pac
- **`packages/adapter-teams`** - Microsoft Teams adapter using `botbuilder`
- **`packages/state-memory`** - In-memory state adapter (for development/testing)
- **`packages/state-redis`** - Redis state adapter (for production)
- **`packages/adapter-whatsapp`** - WhatsApp adapter using Meta Cloud API
- **`packages/integration-tests`** - Integration tests against real platform APIs
- **`examples/nextjs-chat`** - Example Next.js app showing how to use the SDK

Expand Down Expand Up @@ -211,6 +212,7 @@ Key env vars used (see `turbo.json` for full list):
- `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET` - Slack credentials
- `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD`, `TEAMS_APP_TENANT_ID` - Teams credentials
- `GOOGLE_CHAT_CREDENTIALS` or `GOOGLE_CHAT_USE_ADC` - Google Chat auth
- `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_PHONE_NUMBER_ID`, `WHATSAPP_VERIFY_TOKEN` - WhatsApp credentials
- `REDIS_URL` - Redis connection for state adapter
- `BOT_USERNAME` - Default bot username

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![npm downloads](https://img.shields.io/npm/dm/chat)](https://www.npmjs.com/package/chat)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

A unified TypeScript SDK for building chat bots across Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, and Linear. Write your bot logic once, deploy everywhere.
A unified TypeScript SDK for building chat bots across Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp. Write your bot logic once, deploy everywhere.

## Installation

Expand Down Expand Up @@ -56,6 +56,7 @@ See the [Getting Started guide](https://chat-sdk.dev/docs/getting-started) for a
| Telegram | `@chat-adapter/telegram` | Yes | Yes | Partial | No | Post+Edit | Yes |
| GitHub | `@chat-adapter/github` | Yes | Yes | No | No | No | No |
| Linear | `@chat-adapter/linear` | Yes | Yes | No | No | No | No |
| WhatsApp | `@chat-adapter/whatsapp` | N/A | Yes | Partial | No | No | Yes |

## Features

Expand All @@ -82,6 +83,7 @@ See the [Getting Started guide](https://chat-sdk.dev/docs/getting-started) for a
| `@chat-adapter/telegram` | [Telegram adapter](https://chat-sdk.dev/adapters/telegram) |
| `@chat-adapter/github` | [GitHub adapter](https://chat-sdk.dev/adapters/github) |
| `@chat-adapter/linear` | [Linear adapter](https://chat-sdk.dev/adapters/linear) |
| `@chat-adapter/whatsapp` | [WhatsApp adapter](https://chat-sdk.dev/adapters/whatsapp) |
| `@chat-adapter/state-redis` | [Redis state adapter](https://chat-sdk.dev/docs/state/redis) (production) |
| `@chat-adapter/state-ioredis` | [ioredis state adapter](https://chat-sdk.dev/docs/state/ioredis) (alternative) |
| `@chat-adapter/state-pg` | [PostgreSQL state adapter](https://chat-sdk.dev/docs/state/postgres) (production) |
Expand Down
22 changes: 10 additions & 12 deletions apps/docs/adapters.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
"beta": true,
"readme": "https://github.com/vercel/chat/tree/main/packages/adapter-telegram"
},
{
"name": "WhatsApp",
"slug": "whatsapp",
"type": "platform",
"icon": "whatsapp",
"description": "Connect to WhatsApp Business for customer messaging and automated conversations.",
"packageName": "@chat-adapter/whatsapp",
"beta": true,
"readme": "https://github.com/vercel/chat/tree/main/packages/adapter-whatsapp"
},
{
"name": "Redis",
"slug": "redis",
Expand Down Expand Up @@ -172,18 +182,6 @@
"author": "rama-adi",
"readme": "https://github.com/rama-adi/chat-adapter-baileys"
},
{
"name": "WhatsApp",
"slug": "whatsapp",
"type": "platform",
"icon": "whatsapp",
"description": "Connect to WhatsApp Business for customer messaging and automated conversations.",
"comingSoon": true,
"prs": [
"https://github.com/vercel/chat/pull/102",
"https://github.com/vercel/chat/pull/104"
]
},
{
"name": "Instagram",
"slug": "instagram",
Expand Down
6 changes: 4 additions & 2 deletions apps/docs/app/[lang]/(home)/adapters/[slug]/og/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from "node:path";
import { ImageResponse } from "next/og";
import type { NextRequest } from "next/server";
import adapters from "@/adapters.json";
// biome-ignore lint/performance/noNamespaceImport: "Required for Satori"
import * as logos from "@/lib/logos";

const logoSize = 160;
Expand Down Expand Up @@ -46,6 +47,7 @@ const adapterLogos: Record<
width: Math.round(logoSize * (576 / 594)),
height: logoSize,
},
whatsapp: { component: logos.whatsapp, width: logoSize, height: logoSize },
};

const fontsDir = "app/[lang]/og/[...slug]";
Expand Down Expand Up @@ -113,12 +115,12 @@ export const GET = async (
</div>
{adapterLogo ? (
<div
tw="absolute right-[80px] top-0 bottom-0 flex items-center justify-center"
style={{ width: logoSize, height: 628 }}
tw="absolute right-[80px] top-0 bottom-0 flex items-center justify-center"
>
<adapterLogo.component
width={adapterLogo.width}
height={adapterLogo.height}
width={adapterLogo.width}
/>
</div>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
SiInstagram,
SiMessenger,
SiSignal,
SiWhatsapp,
SiX,
} from "@icons-pack/react-simple-icons";
import { ExternalLinkIcon, VerifiedIcon } from "lucide-react";
Expand All @@ -27,6 +26,7 @@ import {
slack,
teams,
telegram,
whatsapp,
} from "@/lib/logos";

const iconMap: Record<
Expand All @@ -44,7 +44,7 @@ const iconMap: Record<
ioredis,
postgres,
memory,
whatsapp: SiWhatsapp,
whatsapp,
instagram: SiInstagram,
signal: SiSignal,
x: SiX,
Expand Down
222 changes: 222 additions & 0 deletions apps/docs/content/docs/adapters/whatsapp.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
---
title: WhatsApp
description: Configure the WhatsApp adapter for the WhatsApp Business Cloud API.
type: integration
prerequisites:
- /docs/getting-started
---

## Installation

```sh title="Terminal"
pnpm add @chat-adapter/whatsapp
```

## Usage

The adapter auto-detects `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_APP_SECRET`, `WHATSAPP_PHONE_NUMBER_ID`, and `WHATSAPP_VERIFY_TOKEN` from environment variables:

```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";
import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";

const bot = new Chat({
userName: "mybot",
adapters: {
whatsapp: createWhatsAppAdapter(),
},
});

bot.onNewMention(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});
```

Since all WhatsApp conversations are 1:1 DMs, every incoming message is treated as a mention.

## Webhook route

WhatsApp uses two webhook mechanisms:

1. **Verification handshake** (GET) — Meta sends a `hub.verify_token` challenge that must match your `WHATSAPP_VERIFY_TOKEN`.
2. **Event delivery** (POST) — incoming messages, reactions, and interactive responses, verified via `X-Hub-Signature-256`.

Both are handled by the same `handleWebhook` method:

```typescript title="app/api/webhooks/whatsapp/route.ts" lineNumbers
import { bot } from "@/lib/bot";

export async function GET(request: Request): Promise<Response> {
return bot.webhooks.whatsapp(request);
}

export async function POST(request: Request): Promise<Response> {
return bot.webhooks.whatsapp(request);
}
```

## Meta app setup

### 1. Create a Meta app

1. Go to [developers.facebook.com/apps](https://developers.facebook.com/apps)
2. Click **Create App** and select **Business** type
3. Give it a name and click **Create App**

### 2. Add WhatsApp product

1. In the app dashboard, find **WhatsApp** and click **Set Up**
2. This creates a test phone number and sandbox environment

### 3. Get credentials

From the app dashboard:

| Credential | Where to find it |
|---|---|
| **Access Token** | WhatsApp > API Setup > Temporary access token (or create a System User token for production) |
| **Phone Number ID** | WhatsApp > API Setup > Phone number ID |
| **App Secret** | Settings > Basic > App Secret |
| **Verify Token** | You define this — any secret string you choose |

### 4. Configure webhooks

1. Go to **WhatsApp** > **Configuration** in your app dashboard
2. Click **Edit** next to Webhook URL
3. Set **Callback URL** to `https://your-domain.com/api/webhooks/whatsapp`
4. Set **Verify token** to the same value as your `WHATSAPP_VERIFY_TOKEN`
5. Click **Verify and Save**
6. Subscribe to the **messages** webhook field

### 5. Production access

For production use:

1. Add a real phone number under **WhatsApp** > **API Setup**
2. Create a **System User** in Meta Business Suite for a permanent access token
3. Complete Meta's **App Review** process for the `whatsapp_business_messaging` permission

## Interactive messages

Card elements are automatically converted to WhatsApp interactive messages:

- **3 or fewer buttons** — rendered as WhatsApp reply buttons (title max 20 characters)
- **More than 3 buttons** — falls back to formatted text

```typescript title="lib/bot.ts" lineNumbers
import { Card, Actions, Button, Body, BodyText } from "chat";

bot.onNewMention(async (thread) => {
await thread.post(
<Card>
<Body>
<BodyText>How can I help?</BodyText>
</Body>
<Actions>
<Button id="help" value="help">Get Help</Button>
<Button id="status" value="status">Check Status</Button>
</Actions>
</Card>
);
});
```

## Media attachments

Incoming media messages (images, documents, audio, video, voice, stickers) are exposed as attachments with a lazy `fetchData()` function. Media is downloaded in two steps via the Graph API — first fetching the media URL, then downloading the binary data.

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
for (const attachment of message.attachments) {
if (attachment.fetchData) {
const data = await attachment.fetchData();
// Process the media buffer...
}
}
});
```

## 24-hour messaging window

WhatsApp enforces a [24-hour customer service window](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages#customer-service-windows). You can only send free-form messages to a user within 24 hours of their last message. After that, you must use approved **message templates**.

## Configuration

All options are auto-detected from environment variables when not provided.

| Option | Required | Description |
|--------|----------|-------------|
| `accessToken` | No* | Meta access token. Auto-detected from `WHATSAPP_ACCESS_TOKEN` |
| `appSecret` | No* | App secret for webhook signature verification. Auto-detected from `WHATSAPP_APP_SECRET` |
| `phoneNumberId` | No* | Bot's phone number ID. Auto-detected from `WHATSAPP_PHONE_NUMBER_ID` |
| `verifyToken` | No* | Secret for webhook verification handshake. Auto-detected from `WHATSAPP_VERIFY_TOKEN` |
| `apiVersion` | No | Graph API version (defaults to `v21.0`) |
| `userName` | No | Bot username. Auto-detected from `WHATSAPP_BOT_USERNAME` or defaults to `whatsapp-bot` |
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |

*All four credentials are required — either via config or environment variables.

## Environment variables

```bash title=".env.local"
WHATSAPP_ACCESS_TOKEN=EAAx... # Meta access token
WHATSAPP_APP_SECRET=abc123... # App secret for signature verification
WHATSAPP_PHONE_NUMBER_ID=1234567890 # Phone number ID from Meta dashboard
WHATSAPP_VERIFY_TOKEN=my-secret # Your chosen webhook verify token
```

## Features

| Feature | Supported |
|---------|-----------|
| Mentions | N/A (all messages are DMs) |
| Reactions (add/remove) | Yes |
| Cards | Interactive messages (max 3 buttons) / text fallback |
| Modals | No |
| Streaming | No |
| DMs | Yes (all conversations) |
| Ephemeral messages | No |
| File uploads | Receive only |
| Typing indicator | Yes |
| Message history | No |
| Edit message | No (throws) |
| Delete message | No (throws) |

## Thread ID format

```
whatsapp:{phoneNumberId}:{userWaId}
```

Example: `whatsapp:1234567890:15551234567`

## Notes

- All WhatsApp conversations are 1:1 DMs between the business phone number and the user, so every message sets `isMention: true`.
- `editMessage()` and `deleteMessage()` throw errors — WhatsApp does not support these operations.
- `fetchMessages()` returns empty results — WhatsApp does not provide a message history API.
- Messages exceeding 4096 characters are automatically split at paragraph or line boundaries.
- Webhook signatures are verified using HMAC-SHA256 with `timingSafeEqual` for timing-attack resistance.

## Troubleshooting

### Webhook verification failing

- Verify `WHATSAPP_VERIFY_TOKEN` matches what you set in the Meta dashboard
- Ensure both GET and POST routes are configured for the webhook URL

### "Invalid signature" error

- Check that `WHATSAPP_APP_SECRET` is correct (find it under Settings > Basic in your Meta app)
- Ensure the raw request body is not parsed before verification

### Bot not responding

- Confirm the **messages** webhook field is subscribed in Meta dashboard
- Check that you're within the 24-hour messaging window (or using template messages)
- Verify the phone number ID matches your configured `WHATSAPP_PHONE_NUMBER_ID`

### Media downloads failing

- Ensure your access token has the required permissions
- Check that the media hasn't expired (WhatsApp media URLs are temporary)
4 changes: 3 additions & 1 deletion apps/docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A unified SDK for building chat bots across Slack, Microsoft Teams,
type: overview
---

Chat SDK is a TypeScript library for building chat bots that work across multiple platforms with a single codebase. Write your bot logic once and deploy it to Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, and Linear.
Chat SDK is a TypeScript library for building chat bots that work across multiple platforms with a single codebase. Write your bot logic once and deploy it to Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp.

## Why Chat SDK?

Expand Down Expand Up @@ -58,6 +58,7 @@ Each adapter factory auto-detects credentials from environment variables (`SLACK
| Telegram | `@chat-adapter/telegram` | Yes | Yes | Partial | No | Post+Edit | Yes |
| GitHub | `@chat-adapter/github` | Yes | Yes | No | No | No | No |
| Linear | `@chat-adapter/linear` | Yes | Yes | No | No | No | No |
| WhatsApp | `@chat-adapter/whatsapp` | N/A | Yes | Partial | No | No | Yes |

## AI coding agent support

Expand All @@ -83,6 +84,7 @@ The SDK is distributed as a set of packages you install based on your needs:
| `@chat-adapter/telegram` | Telegram adapter |
| `@chat-adapter/github` | GitHub Issues adapter |
| `@chat-adapter/linear` | Linear Issues adapter |
| `@chat-adapter/whatsapp` | WhatsApp Business adapter |
| `@chat-adapter/state-redis` | Redis state adapter (production) |
| `@chat-adapter/state-ioredis` | ioredis state adapter (alternative) |
| `@chat-adapter/state-pg` | PostgreSQL state adapter (production) |
Expand Down
Loading