whatsapp-cloud 0.0.4 → 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.
- package/CHANGELOG.md +6 -0
- package/agent_docs/INCOMING_MESSAGES_BRAINSTORM.md +500 -0
- package/cloud-api-docs/webhooks/endpoint.md +112 -0
- package/cloud-api-docs/webhooks/overview.md +154 -0
- package/package.json +6 -2
- package/src/client/HttpClient.ts +43 -6
- package/src/client/WhatsAppClient.ts +3 -0
- package/src/examples/main.ts +9 -0
- package/src/examples/template.ts +134 -0
- package/src/schemas/client.ts +2 -2
- package/src/schemas/index.ts +1 -0
- package/src/schemas/templates/component.ts +145 -0
- package/src/schemas/templates/index.ts +4 -0
- package/src/schemas/templates/request.ts +78 -0
- package/src/schemas/templates/response.ts +64 -0
- package/src/services/accounts/AccountsClient.ts +6 -14
- package/src/services/accounts/AccountsService.ts +19 -21
- package/src/services/accounts/methods/list-phone-numbers.ts +1 -2
- package/src/services/business/BusinessClient.ts +1 -9
- package/src/services/business/BusinessService.ts +19 -21
- package/src/services/business/methods/list-accounts.ts +1 -2
- package/src/services/messages/MessagesClient.ts +2 -6
- package/src/services/messages/MessagesService.ts +42 -22
- package/src/services/templates/TemplatesClient.ts +35 -0
- package/src/services/templates/TemplatesService.ts +117 -0
- package/src/services/templates/index.ts +3 -0
- package/src/services/templates/methods/create.ts +27 -0
- package/src/services/templates/methods/delete.ts +38 -0
- package/src/services/templates/methods/get.ts +23 -0
- package/src/services/templates/methods/list.ts +36 -0
- package/src/services/templates/methods/update.ts +35 -0
- package/src/types/index.ts +1 -0
- package/src/types/templates/component.ts +33 -0
- package/src/types/templates/index.ts +4 -0
- package/src/types/templates/request.ts +28 -0
- package/src/types/templates/response.ts +34 -0
- package/tsconfig.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -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
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Nav-Logo
|
|
2
|
+
Build with us
|
|
3
|
+
Docs
|
|
4
|
+
Blog
|
|
5
|
+
Resources
|
|
6
|
+
Developer centers
|
|
7
|
+
Meine Apps
|
|
8
|
+
Dokumente
|
|
9
|
+
Übersicht
|
|
10
|
+
Webhook-Endpunkt erstellen
|
|
11
|
+
Webhook-Endpunkt erstellen
|
|
12
|
+
Aktualisiert: 07.11.2025
|
|
13
|
+
Erfahre mehr über Webhook-Anfragen und -Antworten, damit du deinen eigenen Webhook-Endpunkt auf einem öffentlichen Server einrichten und konfigurieren kannst.
|
|
14
|
+
Bevor du deine App in einer Produktionsumgebung nutzen kannst, musst du deinen eigenen Webhook-Endpunkt auf einem öffentlichen Server erstellen und konfigurieren, der GET- und POST-Anfragen empfangen und beantworten sowie Webhook-Payloads validieren und erfassen kann.
|
|
15
|
+
TLS/SSL
|
|
16
|
+
Der Server des Webhook-Endpunkts muss über ein korrekt konfiguriertes und installiertes digitales TLS- oder SSL-Sicherheitszertifikat verfügen. Selbstsignierte Zertifikate werden nicht unterstützt.
|
|
17
|
+
mTLS
|
|
18
|
+
Webhooks unterstützen für zusätzliche Sicherheit gegenseitiges TLS (mutual TLS, mTLS). Im Dokument mTLS für Webhooks der Graph API erfährst du, wie du mTLS aktivieren und verwenden kannst.
|
|
19
|
+
Beachte, dass das Aktivieren und Deaktivieren von mTLS nicht auf der Ebene eines WABA oder einer Unternehmenstelefonnummer unterstützt wird. Wenn mehr als eine App auf die Plattform zugreift, musst du mTLS für jede App aktivieren.
|
|
20
|
+
GET-Anfragen
|
|
21
|
+
GET-Anfragen werden verwendet, um deinen Webhook-Endpunkt zu verifizieren. Jedes Mal, wenn du im App-Dashboard das Feld Rückruf-URL oder Verifizierungstoken festlegst oder bearbeitest, senden wir eine GET-Anfrage an deinen Webhook-Endpunkt. Du musst diese Anfrage validieren und auf sie antworten.
|
|
22
|
+
Anfragesyntax
|
|
23
|
+
GET <CALLBACK_URL>
|
|
24
|
+
?hub.mode=subscribe
|
|
25
|
+
&hub.challenge=<HUB.CHALLENGE>
|
|
26
|
+
&hub.verify_token=<HUB.VERIFY_TOKEN>
|
|
27
|
+
Anfrageparameter
|
|
28
|
+
Platzhalter Beschreibung Beispielwert
|
|
29
|
+
<CALLBACK_URL>
|
|
30
|
+
Die URL deines Webhook-Endpunkts
|
|
31
|
+
Füge diese URL im App-Dashboard im Feld Rückruf-URL hinzu, wenn du später Webhooks konfigurierst.
|
|
32
|
+
https://www.luckyshrub.com/webhooks
|
|
33
|
+
<HUB.CHALLENGE>
|
|
34
|
+
Ein zufälliger String, den wir generieren
|
|
35
|
+
1158201444
|
|
36
|
+
<HUB.VERIFY_TOKEN>
|
|
37
|
+
Ein Verifizierungs-String deiner Wahl. Speichere diesen String auf deinem Server.
|
|
38
|
+
Füge diesen String später beim Konfigurieren von Webhooks im Feld Verifizierungstoken im App-Dashboard hinzu.
|
|
39
|
+
vibecoding
|
|
40
|
+
Validierung
|
|
41
|
+
Um GET-Anfragen zu validieren, vergleiche den hub.verify_token-Wert in der Anfrage mit dem Verifizierungs-String, den du auf deinem Server gespeichert hast. Stimmen die Werte überein, ist die Anfrage gültig, andernfalls ist sie ungültig.
|
|
42
|
+
Antwort
|
|
43
|
+
Wenn die Anfrage gültig ist, antworte mit dem HTTP-Status 200 und dem hub.challenge-Wert. Wenn die Anfrage ungültig ist, antworte mit einem HTTP-Statuscode auf der 400er-Ebene oder mit einem anderen Status als 200.
|
|
44
|
+
Wenn du Webhooks konfigurierst, senden wir eine GET-Anfrage an deinen Webhook-Endpunkt. Wenn der Status 200 und der in der Anfrage enthaltene hub.challenge-Wert zurückgesendet werden, betrachten wir deinen Webhook-Endpunkt als verifiziert und beginnen damit, dir Webhooks zu senden. Wenn dein Webhook-Endpunkt mit einer anderen Antwort reagiert, betrachten wir deinen Webhook-Endpunkt jedoch als nicht verifiziert und es werden keine Webhooks an deinen Endpunkt gesendet.
|
|
45
|
+
POST-Anfragen
|
|
46
|
+
Jedes Mal, wenn ein Webhook-Event für Webhook-Felder ausgelöst wird, die du abonniert hast, wird eine POST-Anfrage an deinen Webhook-Endpunkt gesendet. Sie enthält eine JSON-Payload mit einer Beschreibung des Events.
|
|
47
|
+
Anfragesyntax
|
|
48
|
+
POST <CALLBACK_URL>
|
|
49
|
+
Content-Type: application/json
|
|
50
|
+
X-Hub-Signature-256: sha256=<SHA256_PAYLOAD_HASH>
|
|
51
|
+
Content-Length: <CONTENT_LENGTH><JSON_PAYLOAD>
|
|
52
|
+
Anfrageparameter
|
|
53
|
+
Platzhalter Beschreibung Beispielwert
|
|
54
|
+
<CALLBACK_URL>
|
|
55
|
+
Die URL deines Webhook-Endpunkts
|
|
56
|
+
https://www.luckyshrub.com/webhooks
|
|
57
|
+
<CONTENT_LENGTH>
|
|
58
|
+
Inhaltslänge in Byte
|
|
59
|
+
492
|
|
60
|
+
<JSON_PAYLOAD>
|
|
61
|
+
Post-Text-Payload, formatiert als JSON
|
|
62
|
+
In den Referenzen zu den Feldern findest du Beispiel-Payloads.
|
|
63
|
+
<SHA256_PAYLOAD_HASH>
|
|
64
|
+
HMAC-SHA256-Hash, berechnet aus dem Text der POST-Payload und deinem App-Geheimcode als Secret Key.
|
|
65
|
+
b63bb356dff0f1c24379efea2d6ef0b2e2040853339d1bcf13f9018790b1f7d2
|
|
66
|
+
Validierung
|
|
67
|
+
So validierst du die Anfrage:
|
|
68
|
+
Generiere einen HMAC-SHA256-Hash mit der JSON-Payload als Nachrichteneingabe und deinem App-Geheimcode als Secret Key. Vergleiche deinen generierten Hash mit dem Hash, der dem X-Hub-Signature-256-Header (alles nach sha256=) zugewiesen ist.
|
|
69
|
+
Stimmen die Hashes überein, ist die Payload gültig. Erfasse die Payload und verarbeite ihren Inhalt je nach Geschäftsanforderungen. Wenn sie nicht übereinstimmen, kannst du die Payload als ungültig betrachten.
|
|
70
|
+
Beachte, dass wir keine APIs zum Abrufen von Webhook-Verlaufsdaten anbieten. Du musst also entsprechend die Webhook-Payload erfassen und speichern.
|
|
71
|
+
Antwort
|
|
72
|
+
Wenn die Anfrage gültig ist, antworte mit dem HTTP-Status 200. Andernfalls sendest du eine Antwort mit einem HTTP-Status der 400er-Ebene oder mit einem anderen Status als 200.
|
|
73
|
+
Batching
|
|
74
|
+
POST-Anfragen werden in einem Batch mit maximal 1.000 Aktualisierungen aggregiert und gesendet. Die Zusammenfassung in Batches kann jedoch nicht garantiert werden, also passe deine Server so an, dass sie jede POST-Anfrage einzeln verarbeiten können.
|
|
75
|
+
Wenn eine an deinen Server gesendete POST-Anfrage fehlschlägt, wiederholen wir den Vorgang unmittelbar und starten anschließend in immer größeren Abständen innerhalb der nächsten 36 Stunden weitere Wiederholungsversuche. Dein Server sollte in diesen Fällen eine Deduplizierung durchführen können.
|
|
76
|
+
Antworten, die nicht innerhalb von 36 Stunden bestätigt werden, werden gelöscht.
|
|
77
|
+
Webhooks konfigurieren
|
|
78
|
+
Nachdem du deinen Webhook-Endpunkt erstellt hast, navigiere zum Bereich App-Dashboard > WhatsApp > Konfiguration und füge im Feld Rückruf-URL die URL deines Webhook-Endpunkts und im Feld Verifizierungstoken deinen Verifizierungs-String hinzu.
|
|
79
|
+
Hinweis: Wenn du deine App mit dem Anwendungsfall Über WhatsApp mit deinen Kunden in Kontakt treten erstellt hast, navigiere stattdessen zu App-Dashboard > Anwendungsfälle > Anpassen > Konfiguration.
|
|
80
|
+
|
|
81
|
+
Wenn dein Webhook-Endpunkt auf GET-Anfragen zur Webhook-Verifizierung korrekt antwortet, werden deine Änderungen im Bereich gespeichert und es wird eine Liste der Felder angezeigt, die du abonnieren kannst. Anschließend kannst du die Felder abonnieren, die deinen geschäftlichen Anforderungen entsprechen.
|
|
82
|
+
Beachte, dass du den Endpunkt POST Application Subscriptions verwenden kannst, um Webhooks als alternative Methode zu konfigurieren. Dazu ist allerdings ein App-Token erforderlich. Im Dokument Subscriptions-Edge der Graph API erfährst du, wie das geht. Verwende dabei „whatsapp_business_account“ als Objektwert.
|
|
83
|
+
War diese Seite hilfreich?
|
|
84
|
+
„Daumen hoch“-Symbol
|
|
85
|
+
„Daumen runter“-Symbol
|
|
86
|
+
Meta
|
|
87
|
+
FacebookInstagramXLinkedInYouTube
|
|
88
|
+
Build with Meta
|
|
89
|
+
AI
|
|
90
|
+
Meta Horizon
|
|
91
|
+
Social technologies
|
|
92
|
+
Wearables
|
|
93
|
+
News
|
|
94
|
+
Meta for Developers
|
|
95
|
+
Blog
|
|
96
|
+
Success stories
|
|
97
|
+
Support
|
|
98
|
+
Developer Support
|
|
99
|
+
Bug tool
|
|
100
|
+
Platform status
|
|
101
|
+
Developer community forum
|
|
102
|
+
Report an incident
|
|
103
|
+
About us
|
|
104
|
+
About
|
|
105
|
+
Careers
|
|
106
|
+
Terms and policies
|
|
107
|
+
Responsible platform initiatives
|
|
108
|
+
Platform terms
|
|
109
|
+
Developer policies
|
|
110
|
+
Privacy policy
|
|
111
|
+
Cookies
|
|
112
|
+
English (US)
|