whatsapp-cloud 0.0.3 → 0.0.5

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +11 -0
  3. package/agent_docs/DESIGN.md +707 -0
  4. package/agent_docs/INCOMING_MESSAGES_BRAINSTORM.md +500 -0
  5. package/agent_docs/MESSAGES_NAMESPACE_ANALYSIS.md +368 -0
  6. package/agent_docs/NAMING_DECISION.md +78 -0
  7. package/agent_docs/STRUCTURE.md +711 -0
  8. package/agent_docs/messages-namespace-design.md +357 -0
  9. package/cloud-api-docs/webhooks/endpoint.md +112 -0
  10. package/cloud-api-docs/webhooks/overview.md +154 -0
  11. package/package.json +10 -3
  12. package/src/client/HttpClient.ts +159 -0
  13. package/src/client/WhatsAppClient.ts +58 -0
  14. package/src/client/index.ts +2 -0
  15. package/src/errors.ts +58 -0
  16. package/src/examples/main.ts +9 -0
  17. package/src/examples/template.ts +134 -0
  18. package/src/index.ts +16 -1
  19. package/src/schemas/accounts/index.ts +1 -0
  20. package/src/schemas/accounts/phone-number.ts +20 -0
  21. package/src/schemas/business/account.ts +43 -0
  22. package/src/schemas/business/index.ts +2 -0
  23. package/src/schemas/client.ts +50 -0
  24. package/src/schemas/debug.ts +25 -0
  25. package/src/schemas/index.ts +6 -0
  26. package/src/schemas/messages/index.ts +2 -0
  27. package/src/schemas/messages/request.ts +82 -0
  28. package/src/schemas/messages/response.ts +19 -0
  29. package/src/schemas/templates/component.ts +145 -0
  30. package/src/schemas/templates/index.ts +4 -0
  31. package/src/schemas/templates/request.ts +78 -0
  32. package/src/schemas/templates/response.ts +64 -0
  33. package/src/services/accounts/AccountsClient.ts +34 -0
  34. package/src/services/accounts/AccountsService.ts +45 -0
  35. package/src/services/accounts/index.ts +2 -0
  36. package/src/services/accounts/methods/list-phone-numbers.ts +15 -0
  37. package/src/services/business/BusinessClient.ts +34 -0
  38. package/src/services/business/BusinessService.ts +45 -0
  39. package/src/services/business/index.ts +3 -0
  40. package/src/services/business/methods/list-accounts.ts +17 -0
  41. package/src/services/index.ts +2 -0
  42. package/src/services/messages/MessagesClient.ts +34 -0
  43. package/src/services/messages/MessagesService.ts +97 -0
  44. package/src/services/messages/index.ts +8 -0
  45. package/src/services/messages/methods/send-image.ts +33 -0
  46. package/src/services/messages/methods/send-location.ts +32 -0
  47. package/src/services/messages/methods/send-reaction.ts +33 -0
  48. package/src/services/messages/methods/send-text.ts +32 -0
  49. package/src/services/messages/utils/build-message-payload.ts +32 -0
  50. package/src/services/templates/TemplatesClient.ts +35 -0
  51. package/src/services/templates/TemplatesService.ts +117 -0
  52. package/src/services/templates/index.ts +3 -0
  53. package/src/services/templates/methods/create.ts +27 -0
  54. package/src/services/templates/methods/delete.ts +38 -0
  55. package/src/services/templates/methods/get.ts +23 -0
  56. package/src/services/templates/methods/list.ts +36 -0
  57. package/src/services/templates/methods/update.ts +35 -0
  58. package/src/types/accounts/index.ts +1 -0
  59. package/src/types/accounts/phone-number.ts +9 -0
  60. package/src/types/business/account.ts +10 -0
  61. package/src/types/business/index.ts +2 -0
  62. package/src/types/client.ts +8 -0
  63. package/src/types/debug.ts +8 -0
  64. package/src/types/index.ts +6 -0
  65. package/src/types/messages/index.ts +2 -0
  66. package/src/types/messages/request.ts +27 -0
  67. package/src/types/messages/response.ts +7 -0
  68. package/src/types/templates/component.ts +33 -0
  69. package/src/types/templates/index.ts +4 -0
  70. package/src/types/templates/request.ts +28 -0
  71. package/src/types/templates/response.ts +34 -0
  72. package/src/utils/zod-error.ts +28 -0
  73. package/tsconfig.json +6 -4
@@ -0,0 +1,500 @@
1
+ # Incoming Messages & Webhooks - Brainstorming Document
2
+
3
+ ## 🎯 Goal
4
+
5
+ Add incoming message handling to the WhatsApp Cloud SDK. Since this is a **client SDK** (not a server framework), we need to provide:
6
+ 1. **Webhook verification utilities** - Help verify GET requests from Meta
7
+ 2. **Signature validation utilities** - Validate POST request signatures
8
+ 3. **Webhook payload parsing** - Parse and validate incoming webhook payloads
9
+ 4. **Message handlers** - Type-safe handler system for processing incoming messages
10
+ 5. **Type definitions** - Complete type safety for incoming messages
11
+
12
+ ## 📋 Key Requirements from Meta Docs
13
+
14
+ ### Webhook Verification (GET)
15
+ - Meta sends: `GET /webhook?hub.mode=subscribe&hub.challenge=<CHALLENGE>&hub.verify_token=<TOKEN>`
16
+ - We must: Compare `hub.verify_token` with our stored token
17
+ - Response: Return `hub.challenge` as plain text with 200 status if valid
18
+
19
+ ### Webhook Signature Validation (POST)
20
+ - Meta sends: `X-Hub-Signature-256: sha256=<HASH>` header
21
+ - We must: Generate HMAC-SHA256 hash using `app_secret` as key
22
+ - Validation: Compare our hash with the header value
23
+
24
+ ### Webhook Payload Structure
25
+ ```json
26
+ {
27
+ "object": "whatsapp_business_account",
28
+ "entry": [{
29
+ "id": "102290129340398",
30
+ "changes": [{
31
+ "value": {
32
+ "messaging_product": "whatsapp",
33
+ "metadata": {
34
+ "display_phone_number": "15550783881",
35
+ "phone_number_id": "106540352242922"
36
+ },
37
+ "contacts": [{ "profile": { "name": "Sheena Nelson" }, "wa_id": "16505551234" }],
38
+ "messages": [{ "from": "16505551234", "id": "wamid...", "timestamp": "1749416383", "type": "text", "text": { "body": "Hello!" } }],
39
+ "statuses": [{ ... }] // For outgoing message status updates
40
+ },
41
+ "field": "messages"
42
+ }]
43
+ }]
44
+ }
45
+ ```
46
+
47
+ ## 🏗️ Architecture Proposal
48
+
49
+ ### 1. Webhooks Service Namespace
50
+
51
+ Similar to `messages`, `accounts`, `business`, we add a `webhooks` namespace:
52
+
53
+ ```typescript
54
+ const client = new WhatsAppClient({
55
+ accessToken: "...",
56
+ appSecret: "...", // Required for signature validation
57
+ });
58
+
59
+ // Webhook utilities
60
+ client.webhooks.verify(request) // Verify GET request
61
+ client.webhooks.validateSignature(request, body) // Validate POST signature
62
+ client.webhooks.parse(payload) // Parse webhook payload
63
+ ```
64
+
65
+ ### 2. Incoming Message Types & Schemas
66
+
67
+ Create schemas for incoming messages (mirroring outgoing structure):
68
+
69
+ ```
70
+ src/
71
+ ├── schemas/
72
+ │ └── webhooks/
73
+ │ ├── index.ts
74
+ │ ├── payload.ts # WebhookPayloadSchema
75
+ │ ├── incoming-message.ts # IncomingMessage schemas (text, image, audio, etc.)
76
+ │ └── status.ts # Message status update schemas
77
+ ├── types/
78
+ │ └── webhooks/
79
+ │ ├── index.ts
80
+ │ ├── payload.ts
81
+ │ ├── incoming-message.ts
82
+ │ └── status.ts
83
+ ```
84
+
85
+ ### 3. Handler System
86
+
87
+ Provide a handler system that users can implement:
88
+
89
+ ```typescript
90
+ // User's handler implementation
91
+ const handlers = {
92
+ text: async (message: IncomingTextMessage, context: MessageContext) => {
93
+ // Process text message
94
+ return { messages: [{ type: "text", text: { body: "Hello back!" } }] };
95
+ },
96
+ image: async (message: IncomingImageMessage, context: MessageContext) => {
97
+ // Process image message
98
+ return { messages: [] };
99
+ },
100
+ };
101
+
102
+ // SDK provides the router/dispatcher
103
+ const result = await client.webhooks.handle(payload, handlers);
104
+ ```
105
+
106
+ ## 🔧 Implementation Details
107
+
108
+ ### Option A: Framework-Agnostic Utilities (Recommended)
109
+
110
+ **Pros:**
111
+ - Works with any framework (Express, Hono, Next.js, etc.)
112
+ - Users implement their own endpoints
113
+ - SDK provides validation/parsing utilities
114
+
115
+ **Cons:**
116
+ - Users need to wire up routes themselves
117
+ - More boilerplate for users
118
+
119
+ **Structure:**
120
+ ```typescript
121
+ // src/services/webhooks/WebhooksService.ts
122
+ export class WebhooksService {
123
+ /**
124
+ * Verify webhook GET request from Meta
125
+ */
126
+ verify(request: { query: { hub_mode?: string; hub_verify_token?: string; hub_challenge?: string } }, verifyToken: string): string | null {
127
+ if (request.query.hub_mode === "subscribe" && request.query.hub_verify_token === verifyToken) {
128
+ return request.query.hub_challenge || null;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * Validate webhook POST signature
135
+ */
136
+ validateSignature(body: string, signature: string, appSecret: string): boolean {
137
+ // HMAC-SHA256 validation
138
+ }
139
+
140
+ /**
141
+ * Parse webhook payload
142
+ */
143
+ parse(payload: unknown): WebhookPayload {
144
+ // Validate and parse using Zod schema
145
+ }
146
+
147
+ /**
148
+ * Extract incoming messages from payload
149
+ */
150
+ extractMessages(payload: WebhookPayload): IncomingMessage[] {
151
+ // Flatten messages from nested structure
152
+ }
153
+
154
+ /**
155
+ * Extract status updates from payload
156
+ */
157
+ extractStatuses(payload: WebhookPayload): MessageStatus[] {
158
+ // Extract status updates
159
+ }
160
+ }
161
+ ```
162
+
163
+ ### Option B: Framework Adapters
164
+
165
+ **Pros:**
166
+ - Easier to use for specific frameworks
167
+ - Less boilerplate
168
+
169
+ **Cons:**
170
+ - Need to maintain multiple adapters
171
+ - Framework-specific dependencies
172
+
173
+ **Structure:**
174
+ ```typescript
175
+ // src/adapters/express.ts
176
+ export function createExpressWebhookHandler(client: WhatsAppClient, handlers: MessageHandlers) {
177
+ return async (req: Request, res: Response) => {
178
+ // Handle GET verification
179
+ if (req.method === "GET") {
180
+ const challenge = client.webhooks.verify(req.query, process.env.VERIFY_TOKEN);
181
+ if (challenge) return res.send(challenge);
182
+ return res.status(403).send("Forbidden");
183
+ }
184
+
185
+ // Handle POST messages
186
+ const isValid = client.webhooks.validateSignature(req.body, req.headers["x-hub-signature-256"], process.env.APP_SECRET);
187
+ if (!isValid) return res.status(401).send("Invalid signature");
188
+
189
+ const payload = client.webhooks.parse(req.body);
190
+ await client.webhooks.handle(payload, handlers);
191
+ return res.json({ success: true });
192
+ };
193
+ }
194
+ ```
195
+
196
+ ### Option C: Hybrid Approach (Best of Both)
197
+
198
+ Provide utilities + optional framework helpers:
199
+
200
+ ```typescript
201
+ // Core utilities (always available)
202
+ client.webhooks.verify(...)
203
+ client.webhooks.validateSignature(...)
204
+ client.webhooks.parse(...)
205
+
206
+ // Optional framework helpers (separate exports)
207
+ import { createExpressHandler } from "@whatsapp-cloud/express";
208
+ import { createHonoHandler } from "@whatsapp-cloud/hono";
209
+ ```
210
+
211
+ ## 📦 Proposed File Structure
212
+
213
+ ```
214
+ src/
215
+ ├── services/
216
+ │ └── webhooks/
217
+ │ ├── index.ts
218
+ │ ├── WebhooksService.ts # Core service class
219
+ │ ├── WebhooksClient.ts # (if needed, similar to MessagesClient)
220
+ │ ├── utils/
221
+ │ │ ├── verify.ts # GET verification logic
222
+ │ │ ├── validate-signature.ts # POST signature validation
223
+ │ │ ├── parse-payload.ts # Payload parsing
224
+ │ │ └── extract-messages.ts # Extract messages from payload
225
+ │ └── handlers/
226
+ │ ├── index.ts
227
+ │ ├── types.ts # Handler types
228
+ │ └── dispatcher.ts # Message type dispatcher
229
+ ├── schemas/
230
+ │ └── webhooks/
231
+ │ ├── index.ts
232
+ │ ├── payload.ts # WebhookPayloadSchema
233
+ │ ├── incoming-message.ts # Incoming message schemas
234
+ │ └── status.ts # Status update schemas
235
+ └── types/
236
+ └── webhooks/
237
+ ├── index.ts
238
+ ├── payload.ts
239
+ ├── incoming-message.ts
240
+ └── status.ts
241
+ ```
242
+
243
+ ## 🎨 API Design Examples
244
+
245
+ ### Basic Usage (Framework-Agnostic)
246
+
247
+ ```typescript
248
+ import { WhatsAppClient } from "@whatsapp-cloud/sdk";
249
+ import express from "express";
250
+
251
+ const client = new WhatsAppClient({
252
+ accessToken: process.env.ACCESS_TOKEN,
253
+ appSecret: process.env.APP_SECRET, // For signature validation
254
+ });
255
+
256
+ const app = express();
257
+ app.use(express.json());
258
+
259
+ // GET /webhook - Verification
260
+ app.get("/webhook", (req, res) => {
261
+ const challenge = client.webhooks.verify(req.query, process.env.VERIFY_TOKEN);
262
+ if (challenge) {
263
+ res.send(challenge);
264
+ } else {
265
+ res.status(403).send("Forbidden");
266
+ }
267
+ });
268
+
269
+ // POST /webhook - Incoming messages
270
+ app.post("/webhook", async (req, res) => {
271
+ const signature = req.headers["x-hub-signature-256"] as string;
272
+ const bodyString = JSON.stringify(req.body);
273
+
274
+ // Validate signature
275
+ if (!client.webhooks.validateSignature(bodyString, signature, process.env.APP_SECRET)) {
276
+ return res.status(401).send("Invalid signature");
277
+ }
278
+
279
+ // Parse payload
280
+ const payload = client.webhooks.parse(req.body);
281
+
282
+ // Extract messages
283
+ const messages = client.webhooks.extractMessages(payload);
284
+ const statuses = client.webhooks.extractStatuses(payload);
285
+
286
+ // Process messages
287
+ for (const message of messages) {
288
+ if (message.type === "text") {
289
+ // Handle text message
290
+ await client.messages.sendText({
291
+ to: message.from,
292
+ text: { body: "Echo: " + message.text.body },
293
+ });
294
+ }
295
+ }
296
+
297
+ res.json({ success: true });
298
+ });
299
+ ```
300
+
301
+ ### With Handler System
302
+
303
+ ```typescript
304
+ // Define handlers
305
+ const handlers = {
306
+ text: async (message: IncomingTextMessage, context: MessageContext) => {
307
+ console.log(`Received: ${message.text.body}`);
308
+ return {
309
+ messages: [{
310
+ type: "text" as const,
311
+ text: { body: `You said: ${message.text.body}` },
312
+ }],
313
+ };
314
+ },
315
+ image: async (message: IncomingImageMessage, context: MessageContext) => {
316
+ // Download and process image
317
+ const imageData = await client.webhooks.downloadMedia(message.image.id);
318
+ return { messages: [] };
319
+ },
320
+ };
321
+
322
+ // Use dispatcher
323
+ app.post("/webhook", async (req, res) => {
324
+ // ... validation ...
325
+ const payload = client.webhooks.parse(req.body);
326
+ await client.webhooks.handle(payload, handlers);
327
+ res.json({ success: true });
328
+ });
329
+ ```
330
+
331
+ ## 💾 Database Storage Considerations
332
+
333
+ ### When to Store Messages?
334
+
335
+ **Option 1: Store on Send (Outgoing)**
336
+ ```typescript
337
+ // User sends message
338
+ const response = await client.messages.sendText({ to: "+1234567890", text: { body: "Hello" } });
339
+
340
+ // Store immediately
341
+ await db.messages.create({
342
+ id: response.messages[0].id,
343
+ to: "+1234567890",
344
+ from: phoneNumberId,
345
+ body: "Hello",
346
+ status: "sent", // Initial status
347
+ sentAt: new Date(),
348
+ });
349
+ ```
350
+
351
+ **Pros:**
352
+ - Immediate record in database
353
+ - Can track messages even if webhook fails
354
+ - Good for UI showing "sending..." state
355
+
356
+ **Cons:**
357
+ - Status might be outdated (webhook updates it)
358
+ - Need to handle webhook updates separately
359
+
360
+ **Option 2: Store on Webhook (Status Update)**
361
+ ```typescript
362
+ // User sends message
363
+ await client.messages.sendText({ to: "+1234567890", text: { body: "Hello" } });
364
+
365
+ // Don't store yet, wait for webhook
366
+
367
+ // In webhook handler
368
+ app.post("/webhook", async (req, res) => {
369
+ const payload = client.webhooks.parse(req.body);
370
+ const statuses = client.webhooks.extractStatuses(payload);
371
+
372
+ for (const status of statuses) {
373
+ if (status.status === "sent") {
374
+ // Now store the message
375
+ await db.messages.create({
376
+ id: status.id,
377
+ to: status.recipient_id,
378
+ from: phoneNumberId,
379
+ body: "...", // Problem: we don't have the body!
380
+ status: status.status,
381
+ sentAt: new Date(parseInt(status.timestamp) * 1000),
382
+ });
383
+ }
384
+ }
385
+ });
386
+ ```
387
+
388
+ **Pros:**
389
+ - Single source of truth (webhook)
390
+ - Always has accurate status
391
+
392
+ **Cons:**
393
+ - Status webhook doesn't include message body/content
394
+ - Can't show "sending..." state in UI
395
+ - If webhook fails, message is lost
396
+
397
+ **Option 3: Hybrid Approach (Recommended)**
398
+
399
+ ```typescript
400
+ // 1. Store on send with "pending" status
401
+ const response = await client.messages.sendText({ to: "+1234567890", text: { body: "Hello" } });
402
+
403
+ await db.messages.create({
404
+ id: response.messages[0].id,
405
+ to: "+1234567890",
406
+ from: phoneNumberId,
407
+ body: "Hello",
408
+ status: "pending", // Initial status
409
+ sentAt: new Date(),
410
+ });
411
+
412
+ // 2. Update on webhook status
413
+ app.post("/webhook", async (req, res) => {
414
+ const payload = client.webhooks.parse(req.body);
415
+ const statuses = client.webhooks.extractStatuses(payload);
416
+
417
+ for (const status of statuses) {
418
+ await db.messages.update({
419
+ where: { id: status.id },
420
+ data: {
421
+ status: status.status, // "sent", "delivered", "read", "failed"
422
+ deliveredAt: status.status === "delivered" ? new Date(parseInt(status.timestamp) * 1000) : undefined,
423
+ readAt: status.status === "read" ? new Date(parseInt(status.timestamp) * 1000) : undefined,
424
+ },
425
+ });
426
+ }
427
+ });
428
+ ```
429
+
430
+ **For Incoming Messages:**
431
+ ```typescript
432
+ // Always store on webhook (only source of truth)
433
+ app.post("/webhook", async (req, res) => {
434
+ const payload = client.webhooks.parse(req.body);
435
+ const messages = client.webhooks.extractMessages(payload);
436
+
437
+ for (const message of messages) {
438
+ await db.messages.create({
439
+ id: message.id,
440
+ from: message.from,
441
+ to: phoneNumberId,
442
+ type: message.type,
443
+ body: message.type === "text" ? message.text.body : null,
444
+ receivedAt: new Date(parseInt(message.timestamp) * 1000),
445
+ status: "received",
446
+ });
447
+ }
448
+ });
449
+ ```
450
+
451
+ ## 🎯 Recommended Approach
452
+
453
+ ### Phase 1: Core Utilities (MVP)
454
+ 1. ✅ Webhook verification utility
455
+ 2. ✅ Signature validation utility
456
+ 3. ✅ Payload parsing with Zod schemas
457
+ 4. ✅ Type definitions for incoming messages
458
+ 5. ✅ Extract messages/statuses from payload
459
+
460
+ ### Phase 2: Handler System
461
+ 1. ✅ Handler type definitions
462
+ 2. ✅ Message dispatcher/router
463
+ 3. ✅ Media download utility (for images/audio)
464
+
465
+ ### Phase 3: Framework Helpers (Optional)
466
+ 1. Express adapter
467
+ 2. Hono adapter
468
+ 3. Next.js API route helper
469
+
470
+ ## 🤔 Open Questions
471
+
472
+ 1. **Should we include media download utilities?**
473
+ - Yes, it's a common need and requires auth token
474
+ - Provide: `client.webhooks.downloadMedia(mediaId)`
475
+
476
+ 2. **How to handle multiple phone numbers?**
477
+ - Webhook payload includes `phone_number_id` in metadata
478
+ - Users can filter/route based on this
479
+
480
+ 3. **Should we provide a built-in handler dispatcher?**
481
+ - Yes, but make it optional
482
+ - Users can also manually iterate messages
483
+
484
+ 4. **Error handling strategy?**
485
+ - Validation errors throw (invalid signature, malformed payload)
486
+ - Handler errors should be caught and logged (don't break webhook response)
487
+
488
+ 5. **Status updates - separate handler?**
489
+ - Yes, provide separate handler type for status updates
490
+ - Many apps only care about messages, not statuses
491
+
492
+ ## 📝 Next Steps
493
+
494
+ 1. Create schemas for incoming messages (mirror outgoing structure)
495
+ 2. Implement webhook verification utility
496
+ 3. Implement signature validation utility
497
+ 4. Create WebhooksService class
498
+ 5. Add to WhatsAppClient namespace
499
+ 6. Write examples and documentation
500
+