Skip to content

feat: add WhatsApp Business Cloud API adapter#102

Merged
haydenbleasel merged 50 commits intovercel:mainfrom
ghellach:feat/adapter-whatsapp
Mar 10, 2026
Merged

feat: add WhatsApp Business Cloud API adapter#102
haydenbleasel merged 50 commits intovercel:mainfrom
ghellach:feat/adapter-whatsapp

Conversation

@ghellach
Copy link
Contributor

@ghellach ghellach commented Feb 25, 2026

Summary

  • Add @chat-adapter/whatsapp package implementing the WhatsApp Business Cloud API adapter using the Meta Graph API (v21.0, configurable via apiVersion option)
  • Support for sending/receiving text messages, reactions (add/remove), interactive reply buttons (max 3), typing indicators, read receipts, and DMs
  • Webhook verification via GET challenge-response and POST HMAC-SHA256 signature validation
  • Card rendering with interactive buttons when possible, falling back to formatted text
  • Media download support for images, documents, audio, video, voice messages, and stickers via downloadMedia() with lazy fetchData() on attachments
  • Location message support with structured text and Google Maps URLs (with lat/lng validation)
  • Voice message support (separate from audio)
  • Legacy button response handling (template quick replies)
  • Callback data encoding/decoding for interactive reply round-trips
  • Long messages automatically split into multiple messages at paragraph/line boundaries (respecting WhatsApp's 4096-char limit)
  • Escaped formatting character preservation in markdown conversion
  • All WhatsApp DMs treated as mentions (isMention: true) for correct SDK routing
  • Full test suite (78 tests) covering adapter logic, media attachments, card rendering, message splitting, and markdown conversion
  • Documentation and configuration updates across README, CLAUDE.md, turbo.json, vitest workspace, and docs site

Details

Capabilities

Feature Support
Post message Yes (auto-splits long messages)
Edit message No (throws — WhatsApp limitation)
Delete message No (throws — WhatsApp limitation)
Reactions Yes (add and remove)
Typing indicator Yes
DMs Yes (all WhatsApp conversations are 1:1)
Cards/Buttons Partial (max 3 reply buttons, text fallback otherwise)
Media download Yes (images, documents, audio, video, voice, stickers)
Location Yes (with Google Maps URL, lat/lng validated)
Streaming No
Message history No (not exposed by Cloud API)

Media support

Inbound media messages (images, documents, audio, video, voice, stickers) are exposed as Attachment objects on the Message with:

  • type"image", "file", "audio", "video"
  • mimeType — from WhatsApp webhook payload
  • fetchData() — lazy download via two-step Graph API (get URL, then fetch binary)

Location messages include a Google Maps URL and structured text with name/address/coordinates. Latitude and longitude are validated with Number.isFinite() before URL construction.

Thread ID format

whatsapp:{phoneNumberId}:{userWaId}

Environment variables

  • WHATSAPP_ACCESS_TOKEN — Meta Graph API access token
  • WHATSAPP_APP_SECRET — App secret for webhook signature verification
  • WHATSAPP_PHONE_NUMBER_ID — Phone number ID for sending messages
  • WHATSAPP_VERIFY_TOKEN — Token for webhook URL verification

Test plan

  • Unit tests pass for adapter core (encode/decode thread IDs, webhook verification, message parsing)
  • Unit tests pass for media attachments (image, document, audio, video, voice, sticker, location)
  • Unit tests pass for isMention routing
  • Unit tests pass for card rendering (interactive buttons, text fallback, truncation, callback data)
  • Unit tests pass for message splitting (paragraph breaks, line breaks, hard breaks, content preservation)
  • Unit tests pass for markdown conversion (WhatsApp *bold*/~strike~ ↔ standard markdown, escaped chars)
  • pnpm typecheck passes across all packages
  • pnpm check (lint/format) passes
  • pnpm knip (unused exports/deps) passes
  • pnpm test passes (78 WhatsApp adapter tests + all existing tests)
  • Manual testing with WhatsApp Business test account (text, images, reactions, conversations)

@vercel
Copy link
Contributor

vercel bot commented Feb 25, 2026

@ghellach is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@ghellach ghellach marked this pull request as ready for review February 25, 2026 03:34
Copilot AI review requested due to automatic review settings February 25, 2026 03:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new @chat-adapter/whatsapp package that integrates the Chat SDK with the WhatsApp Business Cloud API (Meta Graph API), plus the necessary monorepo wiring (tests, env vars, docs).

Changes:

  • Introduces packages/adapter-whatsapp with adapter implementation, card rendering, markdown conversion, and a dedicated test suite.
  • Wires the new package into the workspace tooling (Vitest workspace, Turbo env passthrough, lockfile).
  • Updates docs/README/CLAUDE.md to include WhatsApp as a supported platform and document its package.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
vitest.workspace.ts Adds the WhatsApp adapter package to the Vitest workspace.
turbo.json Adds WhatsApp-related environment variables to Turbo’s global env passthrough list.
pnpm-lock.yaml Adds the new package importer and updates dependency snapshots accordingly.
packages/adapter-whatsapp/vitest.config.ts Defines Vitest config/coverage settings for the new adapter package.
packages/adapter-whatsapp/tsup.config.ts Adds build configuration for bundling the adapter.
packages/adapter-whatsapp/tsconfig.json Adds TypeScript configuration for the new package.
packages/adapter-whatsapp/src/types.ts Introduces WhatsApp webhook/media/type definitions used by the adapter.
packages/adapter-whatsapp/src/markdown.ts Implements WhatsApp-specific markdown conversion via AST conversion.
packages/adapter-whatsapp/src/markdown.test.ts Tests markdown conversion behavior.
packages/adapter-whatsapp/src/index.ts Implements the WhatsApp adapter: webhook handling, send/reaction/typing/read, thread ID encode/decode, media download, message parsing.
packages/adapter-whatsapp/src/index.test.ts Tests thread ID logic, parsing behavior, attachments, and webhook verification challenge.
packages/adapter-whatsapp/src/cards.ts Converts Card elements into WhatsApp interactive payloads or text fallback.
packages/adapter-whatsapp/src/cards.test.ts Tests card conversion and fallback logic.
packages/adapter-whatsapp/package.json Adds the new package manifest, scripts, and dependencies.
apps/docs/content/docs/index.mdx Adds WhatsApp to the platform list and package table in docs landing page.
apps/docs/content/docs/adapters/index.mdx Adds WhatsApp to the adapters overview matrices and adapter list.
README.md Adds WhatsApp to the supported platforms and adapter list.
CLAUDE.md Adds WhatsApp adapter package and env vars to repo documentation.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@haydenbleasel
Copy link
Member

Nice one @ghellach - jumping in to assist 👍

@vercel
Copy link
Contributor

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview, Comment, Open in v0 Mar 10, 2026 10:11pm
chat-sdk-nextjs-chat Ready Ready Preview, Comment, Open in v0 Mar 10, 2026 10:11pm

@cramforce
Copy link
Contributor

I got this to work on our sample app and made a few fixes:

  • Emoji rendering
  • Streaming must buffer

I'm not sure whether triggering onMention for DMs makes sense. Instead we should trigger a DM

@haydenbleasel
Copy link
Member

Hey @cramforce @ghellach! 👋

Addressed the DM vs mention concern — pushed a commit that adds a new onDirectMessage handler to the Chat SDK.

What changed:

  • New chat.onDirectMessage(handler) method that routes DMs to dedicated handlers
  • Slack and WhatsApp adapters no longer force isMention=true for DMs — routing is now handled by the SDK via adapter.isDM()
  • Fully backward compatible: if no onDirectMessage handlers are registered, DMs still fall through to onNewMention so existing apps aren't affected
  • Added 4 tests covering the new routing logic

The routing priority is: onSubscribedMessage > onDirectMessage > onNewMention > onNewMessage(pattern)

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>
cramforce and others added 2 commits March 8, 2026 11:51
…r 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>
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>
…dicator

- 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>
cramforce and others added 2 commits March 8, 2026 12:08
channel.messages yields newest first but AI expects chronological order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
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>
cramforce and others added 2 commits March 8, 2026 12:24
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>
Also use ━━━ for thematic breaks instead of --- to avoid remark escaping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
@cramforce
Copy link
Contributor

Alright. Fixed markdown conversion and added a reply test

@ghellach
Copy link
Contributor Author

ghellach commented Mar 8, 2026

Hey @haydenbleasel @cramforce thanks for jumping in and improving this one!

CI was failing because the lockfile was out of date. Merged main in to fix that. Also fixed a minor TS error in the replay test.

@haydenbleasel haydenbleasel merged commit 60f5d8e into vercel:main Mar 10, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants