@chat-adapter/whatsapp 4.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vercel, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @chat-adapter/whatsapp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@chat-adapter/whatsapp)](https://www.npmjs.com/package/@chat-adapter/whatsapp)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@chat-adapter/whatsapp)](https://www.npmjs.com/package/@chat-adapter/whatsapp)
5
+
6
+ WhatsApp adapter for [Chat SDK](https://chat-sdk.dev/docs), using the [WhatsApp Business Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api).
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install chat @chat-adapter/whatsapp
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```typescript
17
+ import { Chat } from "chat";
18
+ import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";
19
+
20
+ const bot = new Chat({
21
+ userName: "mybot",
22
+ adapters: {
23
+ whatsapp: createWhatsAppAdapter(),
24
+ },
25
+ });
26
+ ```
27
+
28
+ When using `createWhatsAppAdapter()` without arguments, credentials are auto-detected from environment variables.
29
+
30
+ ## Environment variables
31
+
32
+ | Variable | Required | Description |
33
+ |---|---|---|
34
+ | `WHATSAPP_ACCESS_TOKEN` | Yes | Meta access token (permanent or system user token) |
35
+ | `WHATSAPP_APP_SECRET` | Yes | App secret for X-Hub-Signature-256 verification |
36
+ | `WHATSAPP_PHONE_NUMBER_ID` | Yes | Bot's phone number ID from Meta dashboard |
37
+ | `WHATSAPP_VERIFY_TOKEN` | Yes | User-defined secret for webhook verification handshake |
38
+
39
+ ## Webhook setup
40
+
41
+ WhatsApp uses two webhook mechanisms:
42
+
43
+ 1. **Verification handshake** (GET) — Meta sends a `hub.verify_token` challenge that must match your `WHATSAPP_VERIFY_TOKEN`.
44
+ 2. **Event delivery** (POST) — incoming messages, reactions, and interactive responses, verified via `X-Hub-Signature-256`.
45
+
46
+ ```typescript
47
+ // Next.js App Router example
48
+ import { bot } from "@/lib/bot";
49
+
50
+ export async function GET(request: Request) {
51
+ return bot.adapters.whatsapp.handleWebhook(request);
52
+ }
53
+
54
+ export async function POST(request: Request) {
55
+ return bot.adapters.whatsapp.handleWebhook(request);
56
+ }
57
+ ```
58
+
59
+ ## Interactive messages
60
+
61
+ Card elements are automatically converted to WhatsApp interactive messages:
62
+
63
+ - **3 or fewer buttons** — rendered as WhatsApp reply buttons
64
+ - **More than 3 buttons** — falls back to formatted text
65
+
66
+ ## Limitations
67
+
68
+ - **No message editing** — `editMessage()` throws (WhatsApp limitation)
69
+ - **No message deletion** — `deleteMessage()` throws (WhatsApp limitation)
70
+ - **No message history API** — `fetchMessages()` returns empty results
71
+
72
+ ## Thread ID format
73
+
74
+ ```
75
+ whatsapp:{phoneNumberId}:{userWaId}
76
+ ```
77
+
78
+ Example: `whatsapp:1234567890:15551234567`
79
+
80
+ ## Documentation
81
+
82
+ Full setup instructions at [chat-sdk.dev/docs/adapters/whatsapp](https://chat-sdk.dev/docs/adapters/whatsapp).
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,441 @@
1
+ import { Logger, Adapter, ChatInstance, WebhookOptions, AdapterPostableMessage, RawMessage, StreamChunk, StreamOptions, EmojiValue, FetchOptions, FetchResult, ThreadInfo, Message, FormattedContent } from 'chat';
2
+
3
+ /**
4
+ * Type definitions for the WhatsApp adapter.
5
+ *
6
+ * Based on the WhatsApp Business Cloud API (Meta Graph API).
7
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api
8
+ */
9
+
10
+ /**
11
+ * WhatsApp adapter configuration.
12
+ *
13
+ * Requires a System User access token for API calls and an App Secret
14
+ * for webhook signature verification.
15
+ *
16
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/get-started
17
+ */
18
+ interface WhatsAppAdapterConfig {
19
+ /** Access token (System User token) for WhatsApp Cloud API calls */
20
+ accessToken: string;
21
+ /** Meta Graph API version (default: "v21.0") */
22
+ apiVersion?: string;
23
+ /** Meta App Secret for webhook HMAC-SHA256 signature verification */
24
+ appSecret: string;
25
+ /** Logger instance for error reporting */
26
+ logger: Logger;
27
+ /** WhatsApp Business phone number ID (not the phone number itself) */
28
+ phoneNumberId: string;
29
+ /** Bot display name used for identification */
30
+ userName: string;
31
+ /** Verify token for webhook challenge-response verification */
32
+ verifyToken: string;
33
+ }
34
+ /**
35
+ * Decoded thread ID for WhatsApp.
36
+ *
37
+ * WhatsApp conversations are always 1:1 between a business phone number
38
+ * and a user. There is no concept of threads or channels.
39
+ *
40
+ * Format: whatsapp:{phoneNumberId}:{userWaId}
41
+ */
42
+ interface WhatsAppThreadId {
43
+ /** Business phone number ID */
44
+ phoneNumberId: string;
45
+ /** User's WhatsApp ID (their phone number) */
46
+ userWaId: string;
47
+ }
48
+ /**
49
+ * Contact information from an inbound message.
50
+ */
51
+ interface WhatsAppContact {
52
+ profile: {
53
+ name: string;
54
+ };
55
+ wa_id: string;
56
+ }
57
+ /**
58
+ * Inbound message from a user.
59
+ *
60
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples
61
+ */
62
+ interface WhatsAppInboundMessage {
63
+ /** Audio message content */
64
+ audio?: {
65
+ id: string;
66
+ mime_type: string;
67
+ sha256: string;
68
+ voice?: boolean;
69
+ };
70
+ /** Legacy button response (from template quick replies) */
71
+ button?: {
72
+ payload: string;
73
+ text: string;
74
+ };
75
+ /** Context for quoted replies */
76
+ context?: {
77
+ from: string;
78
+ id: string;
79
+ };
80
+ /** Document message content */
81
+ document?: {
82
+ caption?: string;
83
+ filename?: string;
84
+ id: string;
85
+ mime_type: string;
86
+ sha256: string;
87
+ };
88
+ /** Sender's WhatsApp ID */
89
+ from: string;
90
+ /** Unique message ID */
91
+ id: string;
92
+ /** Image message content */
93
+ image?: {
94
+ caption?: string;
95
+ id: string;
96
+ mime_type: string;
97
+ sha256: string;
98
+ };
99
+ /** Interactive message reply */
100
+ interactive?: {
101
+ button_reply?: {
102
+ id: string;
103
+ title: string;
104
+ };
105
+ list_reply?: {
106
+ description?: string;
107
+ id: string;
108
+ title: string;
109
+ };
110
+ type: "button_reply" | "list_reply";
111
+ };
112
+ /** Location message content */
113
+ location?: {
114
+ address?: string;
115
+ latitude: number;
116
+ longitude: number;
117
+ name?: string;
118
+ url?: string;
119
+ };
120
+ /** Reaction to a message */
121
+ reaction?: {
122
+ emoji: string;
123
+ message_id: string;
124
+ };
125
+ /** Sticker message content */
126
+ sticker?: {
127
+ animated: boolean;
128
+ id: string;
129
+ mime_type: string;
130
+ sha256: string;
131
+ };
132
+ /** Text message content */
133
+ text?: {
134
+ body: string;
135
+ };
136
+ /** Unix timestamp string */
137
+ timestamp: string;
138
+ /** Message type */
139
+ type: "text" | "image" | "document" | "audio" | "video" | "voice" | "sticker" | "location" | "contacts" | "interactive" | "button" | "reaction" | "order" | "system";
140
+ /** Video message content */
141
+ video?: {
142
+ caption?: string;
143
+ id: string;
144
+ mime_type: string;
145
+ sha256: string;
146
+ };
147
+ /** Voice message content */
148
+ voice?: {
149
+ id: string;
150
+ mime_type: string;
151
+ sha256: string;
152
+ };
153
+ }
154
+ /**
155
+ * Response from the media URL endpoint.
156
+ *
157
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#get-media-url
158
+ */
159
+ interface WhatsAppMediaResponse {
160
+ file_size: number;
161
+ id: string;
162
+ messaging_product: "whatsapp";
163
+ mime_type: string;
164
+ sha256: string;
165
+ url: string;
166
+ }
167
+ /**
168
+ * Platform-specific raw message type for WhatsApp.
169
+ */
170
+ interface WhatsAppRawMessage {
171
+ /** Contact info from the webhook */
172
+ contact?: WhatsAppContact;
173
+ /** The raw inbound message data */
174
+ message: WhatsAppInboundMessage;
175
+ /** Phone number ID that received the message */
176
+ phoneNumberId: string;
177
+ }
178
+
179
+ /**
180
+ * Split text into chunks that fit within WhatsApp's message limit,
181
+ * breaking on paragraph boundaries (\n\n) when possible, then line
182
+ * boundaries (\n), and finally at the character limit as a last resort.
183
+ */
184
+ declare function splitMessage(text: string): string[];
185
+
186
+ /**
187
+ * WhatsApp adapter for chat SDK.
188
+ *
189
+ * Supports messaging via the WhatsApp Business Cloud API (Meta Graph API).
190
+ * All conversations are 1:1 DMs between the business phone number and users.
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * import { Chat } from "chat";
195
+ * import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";
196
+ * import { MemoryState } from "@chat-adapter/state-memory";
197
+ *
198
+ * const chat = new Chat({
199
+ * userName: "my-bot",
200
+ * adapters: {
201
+ * whatsapp: createWhatsAppAdapter(),
202
+ * },
203
+ * state: new MemoryState(),
204
+ * });
205
+ * ```
206
+ */
207
+ declare class WhatsAppAdapter implements Adapter<WhatsAppThreadId, WhatsAppRawMessage> {
208
+ readonly name = "whatsapp";
209
+ readonly persistMessageHistory = true;
210
+ readonly userName: string;
211
+ private readonly accessToken;
212
+ private readonly appSecret;
213
+ private readonly phoneNumberId;
214
+ private readonly verifyToken;
215
+ private readonly graphApiUrl;
216
+ private chat;
217
+ private readonly logger;
218
+ private _botUserId;
219
+ private readonly formatConverter;
220
+ /** Bot user ID used for self-message detection */
221
+ get botUserId(): string | undefined;
222
+ constructor(config: WhatsAppAdapterConfig);
223
+ /**
224
+ * Initialize the adapter and fetch business profile info.
225
+ */
226
+ initialize(chat: ChatInstance): Promise<void>;
227
+ /**
228
+ * Handle incoming webhook from WhatsApp.
229
+ *
230
+ * Handles both the GET verification challenge and POST event notifications.
231
+ *
232
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks
233
+ */
234
+ handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
235
+ /**
236
+ * Handle the webhook verification challenge from Meta.
237
+ *
238
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks
239
+ */
240
+ private handleVerificationChallenge;
241
+ /**
242
+ * Verify webhook signature using HMAC-SHA256 with the App Secret.
243
+ *
244
+ * @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verification-requests
245
+ */
246
+ private verifySignature;
247
+ /**
248
+ * Handle an inbound message from a user.
249
+ */
250
+ private handleInboundMessage;
251
+ /**
252
+ * Handle reaction events.
253
+ */
254
+ private handleReaction;
255
+ /**
256
+ * Handle interactive message replies (button/list selection).
257
+ */
258
+ private handleInteractiveReply;
259
+ /**
260
+ * Handle legacy button responses (from template quick replies).
261
+ */
262
+ private handleButtonResponse;
263
+ /**
264
+ * Extract text content from an inbound message.
265
+ * Returns null for unsupported message types.
266
+ */
267
+ private extractTextContent;
268
+ /**
269
+ * Build a Message from a WhatsApp inbound message.
270
+ */
271
+ private buildMessage;
272
+ /**
273
+ * Build attachments from an inbound message.
274
+ */
275
+ private buildAttachments;
276
+ /**
277
+ * Build a single media attachment with a lazy fetchData function.
278
+ */
279
+ private buildMediaAttachment;
280
+ /**
281
+ * Download media from WhatsApp.
282
+ *
283
+ * WhatsApp media is fetched in two steps:
284
+ * 1. GET the media metadata to obtain the download URL
285
+ * 2. GET the actual binary data from the download URL
286
+ *
287
+ * @param mediaId - The media ID from the inbound message
288
+ * @returns The media data as a Buffer
289
+ *
290
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#download-media
291
+ */
292
+ downloadMedia(mediaId: string): Promise<Buffer>;
293
+ /**
294
+ * Send a message to a WhatsApp user.
295
+ *
296
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages
297
+ */
298
+ postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<WhatsAppRawMessage>>;
299
+ /**
300
+ * Split text into chunks that fit within WhatsApp's message limit,
301
+ * breaking on paragraph boundaries (\n\n) when possible, then line
302
+ * boundaries (\n), and finally at the character limit as a last resort.
303
+ */
304
+ splitMessage(text: string): string[];
305
+ /**
306
+ * Send a single text message via the Cloud API (must be within the
307
+ * 4096-character limit).
308
+ */
309
+ private sendSingleTextMessage;
310
+ /**
311
+ * Send a text message, splitting into multiple messages if it exceeds
312
+ * WhatsApp's 4096-character limit. Returns the last message sent.
313
+ */
314
+ private sendTextMessage;
315
+ /**
316
+ * Send an interactive message (buttons or list) via the Cloud API.
317
+ */
318
+ private sendInteractiveMessage;
319
+ /**
320
+ * Edit a message. Not supported by WhatsApp Cloud API — throws an error.
321
+ *
322
+ * Callers should use postMessage directly if they want to send a follow-up.
323
+ */
324
+ editMessage(_threadId: string, _messageId: string, _message: AdapterPostableMessage): Promise<RawMessage<WhatsAppRawMessage>>;
325
+ /**
326
+ * Stream a message by buffering all chunks and sending as a single message.
327
+ * WhatsApp doesn't support message editing, so we can't do incremental updates.
328
+ */
329
+ stream(threadId: string, textStream: AsyncIterable<string | StreamChunk>, _options?: StreamOptions): Promise<RawMessage<WhatsAppRawMessage>>;
330
+ /**
331
+ * Delete a message. Not supported by WhatsApp Cloud API — throws an error.
332
+ */
333
+ deleteMessage(_threadId: string, _messageId: string): Promise<void>;
334
+ /**
335
+ * Add a reaction to a message.
336
+ *
337
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/reaction-messages
338
+ */
339
+ addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
340
+ /**
341
+ * Remove a reaction from a message.
342
+ *
343
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/reaction-messages
344
+ */
345
+ removeReaction(threadId: string, messageId: string, _emoji: EmojiValue | string): Promise<void>;
346
+ /**
347
+ * Start typing indicator.
348
+ *
349
+ * WhatsApp supports typing indicators via the messages endpoint.
350
+ * The indicator displays for up to 25 seconds or until the next message.
351
+ *
352
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/mark-messages-as-read
353
+ */
354
+ startTyping(_threadId: string, _status?: string): Promise<void>;
355
+ /**
356
+ * Fetch messages. Not supported by WhatsApp Cloud API.
357
+ *
358
+ * WhatsApp does not provide an API to retrieve message history.
359
+ */
360
+ fetchMessages(_threadId: string, _options?: FetchOptions): Promise<FetchResult<WhatsAppRawMessage>>;
361
+ /**
362
+ * Fetch thread info.
363
+ */
364
+ fetchThread(threadId: string): Promise<ThreadInfo>;
365
+ /**
366
+ * Encode a WhatsApp thread ID.
367
+ *
368
+ * Format: whatsapp:{phoneNumberId}:{userWaId}
369
+ */
370
+ encodeThreadId(platformData: WhatsAppThreadId): string;
371
+ /**
372
+ * Decode a WhatsApp thread ID.
373
+ *
374
+ * Format: whatsapp:{phoneNumberId}:{userWaId}
375
+ */
376
+ decodeThreadId(threadId: string): WhatsAppThreadId;
377
+ /**
378
+ * Derive channel ID from a WhatsApp thread ID.
379
+ * On WhatsApp every conversation is a 1:1 DM, so channel === thread.
380
+ */
381
+ channelIdFromThreadId(threadId: string): string;
382
+ /**
383
+ * All WhatsApp conversations are DMs.
384
+ */
385
+ isDM(_threadId: string): boolean;
386
+ /**
387
+ * Open a DM with a user. Returns the thread ID for the conversation.
388
+ *
389
+ * For WhatsApp, this simply constructs the thread ID since all
390
+ * conversations are inherently DMs. Note: you can only message users
391
+ * who have messaged you first (within the 24-hour window) or
392
+ * via approved template messages.
393
+ */
394
+ openDM(userId: string): Promise<string>;
395
+ /**
396
+ * Parse platform message format to normalized format.
397
+ */
398
+ parseMessage(raw: WhatsAppRawMessage): Message<WhatsAppRawMessage>;
399
+ /**
400
+ * Render formatted content to WhatsApp markdown.
401
+ */
402
+ renderFormatted(content: FormattedContent): string;
403
+ /**
404
+ * Mark an inbound message as read.
405
+ *
406
+ * @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/mark-messages-as-read
407
+ */
408
+ markAsRead(messageId: string): Promise<void>;
409
+ /**
410
+ * Make a request to the Meta Graph API.
411
+ */
412
+ private graphApiRequest;
413
+ /**
414
+ * Resolve an emoji value to a unicode string.
415
+ */
416
+ private resolveEmoji;
417
+ }
418
+ /**
419
+ * Factory function to create a WhatsApp adapter.
420
+ *
421
+ * @example
422
+ * ```typescript
423
+ * const adapter = createWhatsAppAdapter({
424
+ * accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
425
+ * appSecret: process.env.WHATSAPP_APP_SECRET!,
426
+ * phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
427
+ * verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
428
+ * });
429
+ * ```
430
+ */
431
+ declare function createWhatsAppAdapter(config?: {
432
+ accessToken?: string;
433
+ apiVersion?: string;
434
+ appSecret?: string;
435
+ logger?: Logger;
436
+ phoneNumberId?: string;
437
+ userName?: string;
438
+ verifyToken?: string;
439
+ }): WhatsAppAdapter;
440
+
441
+ export { WhatsAppAdapter, type WhatsAppAdapterConfig, type WhatsAppMediaResponse, type WhatsAppRawMessage, type WhatsAppThreadId, createWhatsAppAdapter, splitMessage };