Add WhatsApp adapter using Meta's WhatsApp Business Cloud#179
Closed
chitru wants to merge 1 commit intovercel:mainfrom
Closed
Add WhatsApp adapter using Meta's WhatsApp Business Cloud#179chitru wants to merge 1 commit intovercel:mainfrom
chitru wants to merge 1 commit intovercel:mainfrom
Conversation
API Adds @chat-adapter/whatsapp with support for:
- Webhook verification (GET handshake + X-Hub-Signature-256)
- Incoming text, media, reaction, and interactive messages
- Outgoing text messages, reactions, and interactive messages
(buttons/lists)
- Thread ID format: whatsapp:{phoneNumberId}:{userPhoneNumber}
- Cache-based message fetching (same pattern as Telegram adapter)
- Error mapping to adapter-specific error types Limitations: edit/delete throw NotImplementedError, typing is a no-op.
Contributor
|
@chitru is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
Member
haydenbleasel
added a commit
to ghellach/chat
that referenced
this pull request
Mar 6, 2026
Bring over several enhancements from chitru's WhatsApp adapter PR (vercel#179): - Voice message support (separate from audio) - Legacy button response handling (template quick replies) - Callback data encoding/decoding for interactive reply round-trips - Message truncation at WhatsApp's 4096 char limit - Example app integration (adapters, webhook route, package.json) - GET webhook forwarding for WhatsApp verification challenges - Package README and changeset - Tests for all new functionality (68 total) Co-Authored-By: Chitru Shrestha <chitra.shrestha@akuru.com.au> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cramforce
pushed a commit
to ghellach/chat
that referenced
this pull request
Mar 8, 2026
Bring over several enhancements from chitru's WhatsApp adapter PR (vercel#179): - Voice message support (separate from audio) - Legacy button response handling (template quick replies) - Callback data encoding/decoding for interactive reply round-trips - Message truncation at WhatsApp's 4096 char limit - Example app integration (adapters, webhook route, package.json) - GET webhook forwarding for WhatsApp verification challenges - Package README and changeset - Tests for all new functionality (68 total) Co-Authored-By: Chitru Shrestha <chitra.shrestha@akuru.com.au> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 tasks
cramforce
pushed a commit
to ghellach/chat
that referenced
this pull request
Mar 8, 2026
Bring over several enhancements from chitru's WhatsApp adapter PR (vercel#179): - Voice message support (separate from audio) - Legacy button response handling (template quick replies) - Callback data encoding/decoding for interactive reply round-trips - Message truncation at WhatsApp's 4096 char limit - Example app integration (adapters, webhook route, package.json) - GET webhook forwarding for WhatsApp verification challenges - Package README and changeset - Tests for all new functionality (68 total) Co-Authored-By: Chitru Shrestha <chitra.shrestha@akuru.com.au> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
haydenbleasel
added a commit
that referenced
this pull request
Mar 10, 2026
* feat: add WhatsApp Business Cloud API adapter Add @chat-adapter/whatsapp with support for sending/receiving messages, reactions, interactive reply buttons, typing indicators, and webhook verification via the Meta Graph API. Includes full test suite, documentation updates, and workspace/turbo configuration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add media download, attachments, and location support to WhatsApp adapter - Add downloadMedia() public method for fetching images, documents, audio, video, and stickers via the Graph API (two-step: URL then binary) - Populate message attachments with lazy fetchData() for all media types - Add location support with Google Maps URL and structured text - Add audio, video, sticker, and location fields to WhatsAppInboundMessage - Set isMention: true on all messages (WhatsApp DMs are always direct) - Update parseMessage to include attachments and isMention - Add 10 new tests covering all media types, locations, and isMention - Update docs feature matrix to reflect media receive support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback for WhatsApp adapter - Validate Graph API response before accessing messages[0].id in sendTextMessage and sendInteractiveMessage - Escape backticks and backslashes in escapeWhatsApp() - Apply escapeWhatsApp() to renderText() content in all style branches - Use webhook phoneNumberId in buildMessage() instead of this.phoneNumberId - Encode proper threadId in parseMessage() instead of empty string - Strict decodeThreadId() validation (exactly 2 segments after prefix) - Add tests for extra segments in decodeThreadId and threadId in parseMessage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Migrate improvements from #179 Bring over several enhancements from chitru's WhatsApp adapter PR (#179): - Voice message support (separate from audio) - Legacy button response handling (template quick replies) - Callback data encoding/decoding for interactive reply round-trips - Message truncation at WhatsApp's 4096 char limit - Example app integration (adapters, webhook route, package.json) - GET webhook forwarding for WhatsApp verification challenges - Package README and changeset - Tests for all new functionality (68 total) Co-Authored-By: Chitru Shrestha <chitra.shrestha@akuru.com.au> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): add error handling for inbound message processing Wrap handleInboundMessage calls in try/catch to log errors if synchronous processing fails (e.g., thread ID encoding). The async processing already has its own error handling in Chat.processMessage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): prevent markdown regex from matching across newlines Use [^\n*] and [^\n~] in fromWhatsAppFormat regex to prevent bold/strike spans from merging across line boundaries. Adds a regression test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): use WhatsAppInteractiveMessage type instead of object Replace the untyped `object` parameter in sendInteractiveMessage with the proper WhatsAppInteractiveMessage type for full type safety. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): hoist emoji mapping to module-level constant Move the emoji name-to-unicode mapping out of resolveEmoji() so it is not re-allocated on every call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): remove duplicate JSDoc comment in types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(example): add startTyping to WhatsApp recording methods The adapter supports typing indicators but the method was missing from the recording proxy list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): fix formatting and add package to readme test allowlist Fix line-length formatting in markdown.ts regex and add @chat-adapter/whatsapp to the valid packages list in readme tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): use defaultEmojiResolver instead of custom emoji map Replace the hand-rolled EMOJI_MAP with the shared defaultEmojiResolver from the chat SDK. WhatsApp uses unicode emoji like GChat, so toGChat() provides the correct mapping with broader coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): make Graph API version configurable Add apiVersion option to WhatsAppAdapterConfig (defaults to v21.0) so users can upgrade without waiting for a package release. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): validate lat/lng before constructing Google Maps URL Coerce and validate latitude/longitude with Number.isFinite() to prevent unexpected URL construction from malformed webhook payloads. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): throw on editMessage instead of silently sending new message Callers expecting an edit would get duplicate messages with the silent fallback. Throwing makes the unsupported operation explicit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): document regex asymmetry between toWhatsApp and fromWhatsApp Explain why toWhatsAppFormat doesn't need newline guards like fromWhatsAppFormat does — the standard markdown parser output never produces spans crossing line boundaries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): document callback data passthrough behavior Add comments explaining that non-prefixed and malformed callback data is intentionally passed through for legacy/external button IDs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): add editMessage and deleteMessage to recording methods Include all adapter methods in the recording list for complete debugging traces, even for unsupported operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): preserve escaped formatting chars in toWhatsAppFormat Escaped asterisks and tildes in standard markdown (e.g. \* and \~) are now preserved through the conversion pipeline so WhatsApp renders them as literal characters instead of misinterpreting them as formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): split long messages instead of truncating Replace silent truncation at 4096 chars with message splitting that breaks on paragraph (\n\n) then line (\n) boundaries, sending multiple messages so no content is lost. Adds 8 tests for the splitting logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(whatsapp): align editMessage/deleteMessage behavior and docs - Fix README: editMessage/deleteMessage both throw, not fallback/no-op - Fix editMessage JSDoc to reflect it throws - Make deleteMessage throw instead of silently warning (consistent with editMessage) - Bump @types/node to ^25.3.2 to match monorepo - Add sample-messages.md with webhook payload examples Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs(whatsapp): add adapter documentation page Add whatsapp.mdx covering installation, usage, Meta app setup, webhook config, interactive messages, media attachments, 24-hour messaging window, configuration, features, and troubleshooting. Also add WhatsApp to the adapters navigation in meta.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add whatsapp adapter debug logging and try/catch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(whatsapp): convert emoji placeholders in outgoing messages WhatsApp adapter was sending raw {{emoji:wave}} placeholders instead of Unicode emoji. Apply convertEmojiPlaceholders on all outgoing paths: text messages, card fallback text, and interactive message fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix button rendering and streaming (needs to buffer) * fix(example): handle editMessage failure on WhatsApp WhatsApp Cloud API doesn't support message editing. Catch the error in the demo "processing" animation and send a follow-up instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(chat): add onDirectMessage handler, stop treating DMs as mentions DMs now route to dedicated onDirectMessage handlers instead of being forced through onNewMention. If no DM handlers registered, DMs fall through to onNewMention for backward compat. Adapters no longer set isMention=true for DMs — the Chat SDK handles routing via adapter.isDM(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(chat): always route DMs to onDirectMessage regardless of subscription Previously, onDirectMessage only fired for unsubscribed DM threads. Subscribed DMs were routed to onSubscribedMessage, which was confusing on non-threaded platforms (WhatsApp, Telegram) where all DMs share one threadId — after the first message, onDirectMessage never fired again. Now, DMs always route to onDirectMessage first, and onSubscribedMessage only handles non-DM subscribed threads. Backward compat is preserved: if no onDirectMessage handlers are registered, DMs fall through as mentions. The example bot is simplified accordingly — onDirectMessage now fetches conversation history via fetchMessages each time instead of relying on subscribe() and stored state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(chat): pass channel as third argument to DirectMessageHandler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(example): reply to channel instead of thread in DM handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(example): use thread instead of channel for DM operations Channel ID is only two parts (whatsapp:{phoneNumberId}) which isn't a valid conversation target on WhatsApp. The thread ID includes the user phone and is required for startTyping/post/fetchMessages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert "fix(example): use thread instead of channel for DM operations" This reverts commit f4801d7. * fix(adapters): return valid thread IDs from channelIdFromThreadId WhatsApp's channelIdFromThreadId was stripping the user WA ID, producing an invalid ID that caused ValidationError on channel operations like startTyping(). Since every WhatsApp conversation is a 1:1 DM, channel and thread are identical. Telegram's channelIdFromThreadId was returning a raw chatId without the telegram: prefix, which is not a valid thread ID for adapter operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(chat): normalize fullStream in Channel.post() to extract text deltas Channel.post() was coercing AI SDK fullStream objects to strings via +=, producing "[object Object]" output. Now uses fromFullStream() to extract text-delta events, matching how Thread.post() already handles streams. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(example): use thread.allMessages for DM history instead of adapter directly The DM handler was calling channel.adapter.fetchMessages() which always returns empty on WhatsApp (no native history API). Now uses thread.allMessages which falls back to the persisted message history cache, giving the AI conversation context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(chat): add message history support to Channel for DM platforms Channel now falls back to the persisted message history cache when the adapter lacks native message fetch (e.g. WhatsApp, Telegram). Incoming messages are persisted under both thread and channel IDs. Outgoing messages from channel.post() are also persisted. The example DM handler now uses channel.messages instead of calling the adapter directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(whatsapp): improve markdown rendering and remove broken typing indicator - Convert headings to bold text, thematic breaks to text separators, and tables to code blocks (WhatsApp doesn't support these) - Convert standard italic (*text*) to WhatsApp italic (_text_) since WhatsApp uses *text* for bold - Make startTyping a no-op (Cloud API doesn't support typing indicators) - Update channelIdFromThreadId test for channel===thread change Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(example): reverse channel.messages to chronological order for AI channel.messages yields newest first but AI expects chronological order. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(chat): auto-sort messages chronologically in toAiMessages toAiMessages now sorts by dateSent (oldest first) so callers don't need to worry about iteration order from channel.messages or thread.messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(chat): pass accumulated stream text as markdown in Channel.post() Stream text was posted as a plain string, bypassing the adapter's format converter. Now wraps it as { markdown: accumulated } so headings, bold, italic etc. are properly converted for each platform. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(whatsapp): use stringifier options for emphasis and bullets Use emphasis: '_' and bullet: '-' options in stringifyMarkdown so the only * in output is **strong**, avoiding conflicts between list bullets and italic markers. Simplifies toWhatsAppFormat to only convert **bold** -> *bold* and ~~strike~~ -> ~strike~. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(whatsapp): flatten bold inside headings to avoid triple asterisks When AI outputs headings with bold text like `## **Choose React if:**`, the heading-to-bold conversion created nested strong nodes producing `***text***`. Now flattens strong children in headings so they merge into a single bold span. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(whatsapp): add full toBe assertion for complex markdown conversion Also use ━━━ for thematic breaks instead of --- to avoid remark escaping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add WhatsApp replay tests from production recordings Adds WhatsApp DM replay test infrastructure: - Fixture from real webhook recordings (dm/whatsapp.json) - WhatsApp test utilities with HMAC-signed request factory and Graph API fetch mock (whatsapp-utils.ts) - 6 replay tests covering DM handling, thread/channel IDs, message sending, status update filtering, sequential messages, and message history persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(whatsapp): fix type narrowing in replay test after merge Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update WhatsApp logo * Update adapters.json * Update logos.tsx --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com> Co-authored-by: Chitru Shrestha <chitra.shrestha@akuru.com.au> Co-authored-by: Malte Ubl <malte.ubl@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
@chat-adapter/whatsapp— a WhatsApp adapter using Meta's official [WhatsApp Business Cloud API (https://developers.facebook.com/docs/whatsapp/cloud-api).Serverless-compatible, zero external dependencies, follows the same patterns as the existing Telegram adapter.
Features
Limitations
NotImplementedErrorNotImplementedErrorEnvironment Variables
WHATSAPP_ACCESS_TOKENWHATSAPP_PHONE_NUMBER_IDWHATSAPP_VERIFY_TOKENWHATSAPP_APP_SECRETTest plan
posting, reactions, interactive messages, error mapping, pagination)
pnpm validatepasses (knip, lint, typecheck, test, build)