ruby-whatsapp 0.4.0 → 0.4.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +276 -634
- data/docs/README.md +73 -0
- data/docs/business_phone_number/README.md +232 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/configuration.md +172 -0
- data/docs/errors.md +208 -0
- data/docs/media/README.md +161 -0
- data/docs/message_templates/README.md +206 -0
- data/docs/message_templates/authentication.md +107 -0
- data/docs/message_templates/carousel.md +103 -0
- data/docs/message_templates/components.md +179 -0
- data/docs/message_templates/library.md +98 -0
- data/docs/message_templates/limited_time_offer.md +94 -0
- data/docs/message_templates/responses.md +173 -0
- data/docs/message_templates/standard.md +138 -0
- data/docs/messages/README.md +135 -0
- data/docs/messages/address.md +86 -0
- data/docs/messages/audio.md +43 -0
- data/docs/messages/contacts.md +129 -0
- data/docs/messages/document.md +49 -0
- data/docs/messages/image.md +59 -0
- data/docs/messages/interactive.md +281 -0
- data/docs/messages/location.md +51 -0
- data/docs/messages/location_request.md +59 -0
- data/docs/messages/mark_message_as_read.md +84 -0
- data/docs/messages/reaction.md +59 -0
- data/docs/messages/sticker.md +40 -0
- data/docs/messages/template.md +166 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +209 -0
- data/docs/webhooks/account_alerts.md +44 -0
- data/docs/webhooks/account_review_update.md +29 -0
- data/docs/webhooks/account_update.md +54 -0
- data/docs/webhooks/automatic_events.md +47 -0
- data/docs/webhooks/business_capability_update.md +40 -0
- data/docs/webhooks/history.md +44 -0
- data/docs/webhooks/message_template_components_update.md +46 -0
- data/docs/webhooks/message_template_quality_update.md +49 -0
- data/docs/webhooks/message_template_status_update.md +55 -0
- data/docs/webhooks/messages.md +385 -0
- data/docs/webhooks/partner_solutions.md +35 -0
- data/docs/webhooks/payment_configuration_update.md +40 -0
- data/docs/webhooks/phone_number_name_update.md +44 -0
- data/docs/webhooks/phone_number_quality_update.md +40 -0
- data/docs/webhooks/security.md +43 -0
- data/docs/webhooks/smb_app_state_sync.md +53 -0
- data/docs/webhooks/smb_message_echoes.md +54 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_preferences.md +48 -0
- data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
- data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
- data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
- data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
- data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
- data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
- data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +11 -12
- data/lib/ruby/whatsapp/business_phone_number.rb +11 -4
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- metadata +63 -1
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# `messages`
|
|
2
|
+
|
|
3
|
+
Inbound customer messages and outbound delivery statuses — the field you will spend
|
|
4
|
+
almost all your time on, and the only one with a Meta-published JSON schema.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::Messages`
|
|
7
|
+
|
|
8
|
+
## A full notification
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"object": "whatsapp_business_account",
|
|
13
|
+
"entry": [{
|
|
14
|
+
"id": "102290129340398",
|
|
15
|
+
"changes": [{
|
|
16
|
+
"field": "messages",
|
|
17
|
+
"value": {
|
|
18
|
+
"messaging_product": "whatsapp",
|
|
19
|
+
"metadata": { "display_phone_number": "15550783881", "phone_number_id": "106540352242922" },
|
|
20
|
+
"contacts": [{ "profile": { "name": "Sheena Nelson" }, "wa_id": "16505551234" }],
|
|
21
|
+
"messages": [{
|
|
22
|
+
"from": "16505551234",
|
|
23
|
+
"id": "wamid.HBg...",
|
|
24
|
+
"timestamp": "1749416383",
|
|
25
|
+
"type": "text",
|
|
26
|
+
"text": { "body": "Does it come in another color?" }
|
|
27
|
+
}]
|
|
28
|
+
}
|
|
29
|
+
}]
|
|
30
|
+
}]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
value = notification.entry.first.changes.first.value
|
|
36
|
+
|
|
37
|
+
value.messaging_product # => "whatsapp"
|
|
38
|
+
value.metadata.phone_number_id # => "106540352242922"
|
|
39
|
+
value.metadata.display_phone_number # => "15550783881"
|
|
40
|
+
value.contacts.first.profile_name # => "Sheena Nelson"
|
|
41
|
+
value.contacts.first.wa_id # => "16505551234"
|
|
42
|
+
value.messages.first.body # => "Does it come in another color?"
|
|
43
|
+
value.statuses # => []
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Value accessors
|
|
47
|
+
|
|
48
|
+
| Accessor | Type |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `messaging_product` | `"whatsapp"` |
|
|
51
|
+
| `metadata` | `Metadata` — `display_phone_number`, `phone_number_id` |
|
|
52
|
+
| `contacts` | `Array<Contact>` — `profile_name`, `wa_id` |
|
|
53
|
+
| `messages` | `Array<Message::*>` |
|
|
54
|
+
| `statuses` | `Array<Status>` |
|
|
55
|
+
|
|
56
|
+
A given notification carries **either** `messages` **or** `statuses`, never both — but
|
|
57
|
+
both accessors always return an array, so you can iterate either without a nil check.
|
|
58
|
+
|
|
59
|
+
> `contacts` here is the value-level sender identity (who wrote to you). It is a
|
|
60
|
+
> different thing from [`Message::Contacts`](#contacts) below, which is a contact
|
|
61
|
+
> *card* someone shared with you.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Message types
|
|
66
|
+
|
|
67
|
+
Every entry in `messages` is dispatched by its `type` through the frozen
|
|
68
|
+
`Message::MESSAGE_TYPES` registry. An unrecognized type falls back to
|
|
69
|
+
[`Unknown`](#unknown), never an exception.
|
|
70
|
+
|
|
71
|
+
All message classes share this envelope from `Message::Base`:
|
|
72
|
+
|
|
73
|
+
| Accessor | Meaning |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `from` | The customer's WhatsApp ID |
|
|
76
|
+
| `id` | The WAMID — pass it to [`mark_message_as_read!`](../messages/mark_message_as_read.md) |
|
|
77
|
+
| `timestamp` | Unix seconds, as a String |
|
|
78
|
+
| `type` | `"text"`, `"image"`, … |
|
|
79
|
+
| `context` | [`Context`](#context) or `nil` — set when replying or forwarding |
|
|
80
|
+
| `referral` | [`Referral`](#referral) or `nil` — set when the chat started from an ad |
|
|
81
|
+
|
|
82
|
+
### Text
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{ "type": "text", "text": { "body": "Does it come in another color?" } }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
message.body # => "Does it come in another color?"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Media — image, video, audio, document, sticker
|
|
93
|
+
|
|
94
|
+
All five share a `Media` superclass:
|
|
95
|
+
|
|
96
|
+
| Accessor | Meaning |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `media_id` | Pass to [`Media#get_url`](../media/README.md) to download it |
|
|
99
|
+
| `mime_type` | `"image/jpeg"`, `"application/pdf"`, … |
|
|
100
|
+
| `sha256` | Content hash |
|
|
101
|
+
| `caption` | Present on image, video, document |
|
|
102
|
+
|
|
103
|
+
Plus one extra each on three of them:
|
|
104
|
+
|
|
105
|
+
| Type | Extra |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `audio` | `voice` — `true` for a recorded voice note, `false` for an audio file |
|
|
108
|
+
| `document` | `filename` |
|
|
109
|
+
| `sticker` | `animated` |
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{ "type": "document",
|
|
113
|
+
"document": { "id": "med.4", "mime_type": "application/pdf",
|
|
114
|
+
"sha256": "abc", "caption": "Invoice", "filename": "invoice.pdf" } }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
message.media_id # => "med.4"
|
|
119
|
+
message.mime_type # => "application/pdf"
|
|
120
|
+
message.filename # => "invoice.pdf"
|
|
121
|
+
|
|
122
|
+
# fetch the bytes
|
|
123
|
+
info = Whatsapp::Media.new.get_url(media_id: message.media_id)
|
|
124
|
+
Whatsapp::Media.new.download(url: info["url"], save_to: message.filename)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{ "type": "audio", "audio": { "id": "med.3", "mime_type": "audio/ogg", "sha256": "abc", "voice": true } }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
message.voice # => true
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Location
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{ "type": "location",
|
|
139
|
+
"location": { "latitude": 37.4847, "longitude": -122.1477,
|
|
140
|
+
"name": "Meta HQ", "address": "1 Hacker Way" } }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
message.latitude # => 37.4847
|
|
145
|
+
message.longitude # => -122.1477
|
|
146
|
+
message.name # => "Meta HQ"
|
|
147
|
+
message.address # => "1 Hacker Way"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This is what comes back from a
|
|
151
|
+
[location request](../messages/location_request.md).
|
|
152
|
+
|
|
153
|
+
### Contacts
|
|
154
|
+
|
|
155
|
+
A contact *card* the customer shared with you.
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{ "type": "contacts",
|
|
159
|
+
"contacts": [{ "name": { "formatted_name": "Jane Doe" },
|
|
160
|
+
"phones": [{ "phone": "+1", "type": "WORK" }] }] }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
card = message.contacts.first
|
|
165
|
+
card.name.formatted_name # => "Jane Doe"
|
|
166
|
+
card.phones.first.phone # => "+1"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Nested objects mirror the outbound [contacts message](../messages/contacts.md)
|
|
170
|
+
field-for-field: `Name`, `Phone`, `Email`, `Address`, `Org`, `Url`, plus `birthday`.
|
|
171
|
+
|
|
172
|
+
### Interactive
|
|
173
|
+
|
|
174
|
+
A tap on [reply buttons or a list](../messages/interactive.md).
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{ "type": "interactive",
|
|
178
|
+
"interactive": { "type": "button_reply",
|
|
179
|
+
"button_reply": { "id": "confirm", "title": "Confirm" } } }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
message.interactive_type # => "button_reply" (or "list_reply")
|
|
184
|
+
message.reply_id # => "confirm" — the id you set when sending
|
|
185
|
+
message.title # => "Confirm"
|
|
186
|
+
message.description # => nil — list_reply only
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{ "type": "interactive",
|
|
191
|
+
"interactive": { "type": "list_reply",
|
|
192
|
+
"list_reply": { "id": "espresso", "title": "Espresso",
|
|
193
|
+
"description": "Strong & short" } } }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
message.interactive_type # => "list_reply"
|
|
198
|
+
message.reply_id # => "espresso"
|
|
199
|
+
message.description # => "Strong & short"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Button
|
|
203
|
+
|
|
204
|
+
A tap on a **template** quick-reply button — distinct from `interactive` above, which
|
|
205
|
+
covers interactive messages.
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{ "type": "button", "button": { "text": "Confirm", "payload": "CONFIRM_PAYLOAD" } }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
message.text # => "Confirm"
|
|
213
|
+
message.payload # => "CONFIRM_PAYLOAD"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Order
|
|
217
|
+
|
|
218
|
+
A cart submitted from your Meta catalog.
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{ "type": "order",
|
|
222
|
+
"order": { "catalog_id": "cat.1", "text": "Here's my order",
|
|
223
|
+
"product_items": [{ "product_retailer_id": "sku.1", "quantity": 2,
|
|
224
|
+
"item_price": 9.99, "currency": "USD" }] } }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
message.catalog_id # => "cat.1"
|
|
229
|
+
message.text # => "Here's my order"
|
|
230
|
+
item = message.product_items.first
|
|
231
|
+
item.product_retailer_id # => "sku.1"
|
|
232
|
+
item.quantity # => 2
|
|
233
|
+
item.item_price # => 9.99
|
|
234
|
+
item.currency # => "USD"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### System
|
|
238
|
+
|
|
239
|
+
A change to the customer's account — a new phone number, an identity change.
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{ "type": "system",
|
|
243
|
+
"system": { "body": "Jane changed to a new phone", "identity": "ABCD1234",
|
|
244
|
+
"wa_id": "16505551234", "type": "customer_changed_number" } }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
message.body # => "Jane changed to a new phone"
|
|
249
|
+
message.identity # => "ABCD1234"
|
|
250
|
+
message.wa_id # => "16505551234"
|
|
251
|
+
message.change_type # => "customer_changed_number"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Note `change_type`, not `type` — `type` is taken by the message envelope.
|
|
255
|
+
|
|
256
|
+
### Reaction
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{ "type": "reaction", "reaction": { "message_id": "wamid.OLD", "emoji": "👍" } }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
message.message_id # => "wamid.OLD" — the message being reacted to
|
|
264
|
+
message.emoji # => "👍" — empty String when a reaction is removed
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Unknown
|
|
268
|
+
|
|
269
|
+
The fallback for a type this gem doesn't model — including anything Meta flags as
|
|
270
|
+
unsupported. It carries the errors and the whole raw hash, so nothing is lost.
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{ "from": "16505551234", "type": "unsupported_future_type",
|
|
274
|
+
"errors": [{ "code": 131051, "title": "Unsupported message type", "message": "m" }] }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
message.type # => "unsupported_future_type"
|
|
279
|
+
message.errors.first.code # => 131051
|
|
280
|
+
message.errors.first.title # => "Unsupported message type"
|
|
281
|
+
message.raw # => the complete hash
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Context
|
|
287
|
+
|
|
288
|
+
Present when the customer replied to, or forwarded, a message.
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
message.context&.id # => "wamid.HBg..." — the message replied to
|
|
292
|
+
message.context&.from # => the sender of that message
|
|
293
|
+
message.context&.forwarded # => true | false
|
|
294
|
+
message.context&.frequently_forwarded # => true | false
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`context` is `nil` — not an all-nil object — when the key is absent, so `&.` is the
|
|
298
|
+
right idiom.
|
|
299
|
+
|
|
300
|
+
## Referral
|
|
301
|
+
|
|
302
|
+
Present when the conversation started from a click-to-WhatsApp ad or post.
|
|
303
|
+
|
|
304
|
+
| Accessor | Meaning |
|
|
305
|
+
| --- | --- |
|
|
306
|
+
| `source_url` | The ad or post URL |
|
|
307
|
+
| `source_type` | `"ad"` or `"post"` |
|
|
308
|
+
| `source_id` | The ad or post ID |
|
|
309
|
+
| `headline` | Ad headline |
|
|
310
|
+
| `body` | Ad body |
|
|
311
|
+
| `media_type` | `"image"` or `"video"` |
|
|
312
|
+
| `image_url` `video_url` `thumbnail_url` | Creative assets |
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
message.referral&.source_type # => "ad"
|
|
316
|
+
message.referral&.headline # => "Big sale"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Statuses
|
|
322
|
+
|
|
323
|
+
Delivery receipts for messages **you** sent. They arrive on the same `messages` field.
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
{ "id": "wamid.HBg", "status": "delivered", "timestamp": "1750263773",
|
|
327
|
+
"recipient_id": "16505551234",
|
|
328
|
+
"conversation": { "id": "6ceb9d9", "origin": { "type": "service" } },
|
|
329
|
+
"pricing": { "billable": true, "pricing_model": "CBP", "category": "service" },
|
|
330
|
+
"errors": [{ "code": 131026, "title": "Message undeliverable", "message": "m" }] }
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
status = value.statuses.first
|
|
335
|
+
|
|
336
|
+
status.id # => "wamid.HBg" — matches what send! returned
|
|
337
|
+
status.status # => "delivered" — sent | delivered | read | failed
|
|
338
|
+
status.timestamp # => "1750263773"
|
|
339
|
+
status.recipient_id # => "16505551234"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
| Accessor | Type |
|
|
343
|
+
| --- | --- |
|
|
344
|
+
| `conversation` | `Status::Conversation` — `id`, `origin_type`, `expiration_timestamp` |
|
|
345
|
+
| `pricing` | `Status::Pricing` — `billable`, `pricing_model`, `category` |
|
|
346
|
+
| `errors` | `Array<Webhook::Error>` — populated on `"failed"` |
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
status.conversation.id # => "6ceb9d9"
|
|
350
|
+
status.conversation.origin_type # => "service" | marketing | utility | authentication
|
|
351
|
+
status.conversation.expiration_timestamp # => when the 24-hour window closes
|
|
352
|
+
|
|
353
|
+
status.pricing.billable # => true
|
|
354
|
+
status.pricing.pricing_model # => "CBP"
|
|
355
|
+
status.pricing.category # => "service"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Correlating a status back to your own record:
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
response = Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi")
|
|
362
|
+
OutboundMessage.create!(wamid: response.messages.first.id)
|
|
363
|
+
|
|
364
|
+
# ... later, in the webhook handler
|
|
365
|
+
value.statuses.each do |status|
|
|
366
|
+
OutboundMessage.find_by(wamid: status.id)&.update!(state: status.status)
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Errors
|
|
371
|
+
|
|
372
|
+
`Whatsapp::Webhook::Error` is a **value object**, not an exception — it models Meta's
|
|
373
|
+
error payloads on `Status#errors` and `Message::Unknown#errors`.
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
error = status.errors.first
|
|
377
|
+
error.code # => 131026
|
|
378
|
+
error.title # => "Message undeliverable"
|
|
379
|
+
error.message # => "m"
|
|
380
|
+
error.details # => dug from error_data.details
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# `partner_solutions`
|
|
2
|
+
|
|
3
|
+
Lifecycle events for a Tech Provider "solution" — the packaged onboarding a partner
|
|
4
|
+
offers to its own customers.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::PartnerSolutions` · confidence: **low**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "partner_solutions", "value": { "solution_id": "sol.1", "event": "DISCONNECTED" } }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Accessors
|
|
15
|
+
|
|
16
|
+
| Accessor | Meaning |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `solution_id` | The solution's ID |
|
|
19
|
+
| `event` | e.g. `CONNECTED`, `DISCONNECTED` |
|
|
20
|
+
|
|
21
|
+
## Handling it
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
when "partner_solutions"
|
|
25
|
+
Solution.find_by(meta_id: change.value.solution_id)&.update!(state: change.value.event)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Relevant only if you operate as a Tech Provider. Most integrations never see it.
|
|
29
|
+
|
|
30
|
+
> **Best-effort schema, low confidence.** Validate against a real payload before
|
|
31
|
+
> depending on it in production.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `payment_configuration_update`
|
|
2
|
+
|
|
3
|
+
A payment provider configuration changed — the plumbing behind in-chat payments,
|
|
4
|
+
currently a regional feature (India, Brazil, Singapore).
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::PaymentConfigurationUpdate` · confidence: **moderate**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "payment_configuration_update",
|
|
12
|
+
"value": { "configuration_name": "default",
|
|
13
|
+
"provider_name": "razorpay",
|
|
14
|
+
"provider_mid": "mid.1",
|
|
15
|
+
"status": "ACTIVE" } }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Accessors
|
|
19
|
+
|
|
20
|
+
| Accessor | Meaning |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `configuration_name` | Your name for the configuration |
|
|
23
|
+
| `provider_name` | e.g. `razorpay`, `payu` |
|
|
24
|
+
| `provider_mid` | The provider's merchant ID |
|
|
25
|
+
| `status` | e.g. `ACTIVE`, `INACTIVE` |
|
|
26
|
+
|
|
27
|
+
## Handling it
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
when "payment_configuration_update"
|
|
31
|
+
config = change.value
|
|
32
|
+
PaymentConfig.find_by(name: config.configuration_name)&.update!(status: config.status)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> **Best-effort schema.** Meta publishes no JSON example for this field. Validate
|
|
36
|
+
> against a real payload before depending on it in production.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# `phone_number_name_update`
|
|
2
|
+
|
|
3
|
+
The verdict on a display-name change you requested for a phone number. The display
|
|
4
|
+
name is what customers see as the sender, and Meta reviews every change.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::PhoneNumberNameUpdate` · confidence: **moderate-high**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "phone_number_name_update",
|
|
12
|
+
"value": { "phone_number": "15550783881",
|
|
13
|
+
"decision": "REJECTED",
|
|
14
|
+
"requested_verified_name": "Acme Corp",
|
|
15
|
+
"rejection_reason": "INCLUDES_UNSUPPORTED_CHARACTERS" } }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Accessors
|
|
19
|
+
|
|
20
|
+
| Accessor | Meaning |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `phone_number` | The number affected |
|
|
23
|
+
| `decision` | `APPROVED` or `REJECTED` |
|
|
24
|
+
| `requested_verified_name` | The name you asked for |
|
|
25
|
+
| `rejection_reason` | Why it was refused, when it was |
|
|
26
|
+
|
|
27
|
+
## Handling it
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
when "phone_number_name_update"
|
|
31
|
+
update = change.value
|
|
32
|
+
|
|
33
|
+
if update.decision == "REJECTED"
|
|
34
|
+
Ops.notify("Display name #{update.requested_verified_name.inspect} rejected: " \
|
|
35
|
+
"#{update.rejection_reason}")
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **Best-effort schema.** Meta publishes no JSON example for this field. Validate
|
|
40
|
+
> against a real payload before depending on it in production.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `phone_number_quality_update`
|
|
2
|
+
|
|
3
|
+
A phone number's quality rating or messaging tier changed. A downgrade cuts how many
|
|
4
|
+
conversations that number may start per day, so this is worth alerting on.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::PhoneNumberQualityUpdate` · confidence: **moderate**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "phone_number_quality_update",
|
|
12
|
+
"value": { "display_phone_number": "15550783881",
|
|
13
|
+
"event": "DOWNGRADE",
|
|
14
|
+
"current_limit": "TIER_50" } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Accessors
|
|
18
|
+
|
|
19
|
+
| Accessor | Meaning |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `display_phone_number` | The number affected |
|
|
22
|
+
| `event` | e.g. `UPGRADE`, `DOWNGRADE`, `FLAGGED`, `UNFLAGGED` |
|
|
23
|
+
| `current_limit` | e.g. `TIER_50`, `TIER_250`, `TIER_1K`, `TIER_UNLIMITED` |
|
|
24
|
+
|
|
25
|
+
## Handling it
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
when "phone_number_quality_update"
|
|
29
|
+
quality = change.value
|
|
30
|
+
|
|
31
|
+
Ops.alert("#{quality.display_phone_number} downgraded to #{quality.current_limit}") \
|
|
32
|
+
if quality.event == "DOWNGRADE"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> **Best-effort schema.** Meta publishes no JSON example for this field. Validate
|
|
36
|
+
> against a real payload before depending on it in production.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# `security`
|
|
2
|
+
|
|
3
|
+
Security events on a phone number — chiefly two-step verification being enabled or
|
|
4
|
+
disabled.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::Security` · confidence: **low**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "security",
|
|
12
|
+
"value": { "display_phone_number": "15550783881",
|
|
13
|
+
"event": "TWO_STEP_VERIFICATION_ENABLED",
|
|
14
|
+
"requester": "16505551234" } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Accessors
|
|
18
|
+
|
|
19
|
+
| Accessor | Meaning |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `display_phone_number` | The number affected |
|
|
22
|
+
| `event` | e.g. `TWO_STEP_VERIFICATION_ENABLED` / `..._DISABLED` |
|
|
23
|
+
| `requester` | Who initiated the change |
|
|
24
|
+
|
|
25
|
+
## Handling it
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
when "security"
|
|
29
|
+
event = change.value
|
|
30
|
+
AuditLog.create!(kind: event.event, subject: event.display_phone_number,
|
|
31
|
+
actor: event.requester)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The two-step verification PIN this concerns is the same one
|
|
35
|
+
[`Register`](../business_phone_number/README.md#registering) consumes.
|
|
36
|
+
|
|
37
|
+
> **Best-effort schema, low confidence.** Meta describes this field in one line and
|
|
38
|
+
> publishes no JSON example. Validate against a real payload before depending on it in
|
|
39
|
+
> production.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# `smb_app_state_sync`
|
|
2
|
+
|
|
3
|
+
Contact-book and app-state changes synced from the WhatsApp Business app. Arrives
|
|
4
|
+
alongside [`history`](history.md) when a number moves onto Cloud API.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::SmbAppStateSync` · confidence: **low-moderate**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "smb_app_state_sync",
|
|
12
|
+
"value": { "state_sync": [{ "type": "contact",
|
|
13
|
+
"action": "add",
|
|
14
|
+
"contact": { "full_name": "Jane Doe", "phone_number": "+1" } }] } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Accessors
|
|
18
|
+
|
|
19
|
+
| Accessor | Type |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `state_sync` | `Array<SmbAppStateSync::StateSync>` |
|
|
22
|
+
|
|
23
|
+
### `StateSync`
|
|
24
|
+
|
|
25
|
+
| Accessor | Meaning |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `type` | e.g. `contact` |
|
|
28
|
+
| `action` | e.g. `add`, `update`, `remove` |
|
|
29
|
+
| `contact` | **Raw Hash** — the shape is undocumented |
|
|
30
|
+
|
|
31
|
+
## Handling it
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
when "smb_app_state_sync"
|
|
35
|
+
change.value.state_sync.each do |sync|
|
|
36
|
+
next unless sync.type == "contact"
|
|
37
|
+
|
|
38
|
+
case sync.action
|
|
39
|
+
when "add", "update"
|
|
40
|
+
Contact.upsert_from_meta(sync.contact)
|
|
41
|
+
when "remove"
|
|
42
|
+
Contact.find_by(phone: sync.contact["phone_number"])&.destroy
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Best-effort schema, low-moderate confidence.** `contact` is left as a raw hash
|
|
48
|
+
> precisely because its keys are unverified. Validate against a real payload before
|
|
49
|
+
> depending on it in production.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# `smb_message_echoes`
|
|
2
|
+
|
|
3
|
+
Echoes of messages sent **from the WhatsApp Business app** rather than through the API.
|
|
4
|
+
Without this, a number used by both your code and a human agent in the app would show
|
|
5
|
+
you only half the conversation.
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::SmbMessageEchoes` · confidence: **moderate**
|
|
8
|
+
|
|
9
|
+
## Payload
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "field": "smb_message_echoes",
|
|
13
|
+
"value": { "messaging_product": "whatsapp",
|
|
14
|
+
"metadata": { "display_phone_number": "15550783881",
|
|
15
|
+
"phone_number_id": "106540352242922" },
|
|
16
|
+
"message_echoes": [{ "from": "15550783881",
|
|
17
|
+
"type": "text",
|
|
18
|
+
"text": { "body": "Thanks for shopping with us!" } }] } }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Accessors
|
|
22
|
+
|
|
23
|
+
| Accessor | Type |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `messaging_product` | `"whatsapp"` |
|
|
26
|
+
| `metadata` | `Metadata` — `display_phone_number`, `phone_number_id` |
|
|
27
|
+
| `message_echoes` | `Array<Message::*>` |
|
|
28
|
+
|
|
29
|
+
`message_echoes` reuses the same `Message.deserialize` dispatcher as
|
|
30
|
+
[`messages`](messages.md), so every echoed message is a fully typed `Message::Text`,
|
|
31
|
+
`Message::Image`, and so on — with the same accessors.
|
|
32
|
+
|
|
33
|
+
## Handling it
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
when "smb_message_echoes"
|
|
37
|
+
change.value.message_echoes.each do |echo|
|
|
38
|
+
Conversation.for(echo.from).messages.create!(
|
|
39
|
+
wamid: echo.id, direction: :outbound, source: :business_app, body: echo.try(:body)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Note `from` is *your* business number here, not the customer's — these are messages
|
|
45
|
+
you sent.
|
|
46
|
+
|
|
47
|
+
> **Moderate confidence on structure, low on the key name.** Meta states these mirror
|
|
48
|
+
> the standard message shape, which is why `Message.deserialize` is reused, but the
|
|
49
|
+
> exact `message_echoes` key is unverified. Validate against a real payload before
|
|
50
|
+
> depending on it in production.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|