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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +276 -634
  4. data/docs/README.md +73 -0
  5. data/docs/business_phone_number/README.md +232 -0
  6. data/docs/business_phone_number/account.md +170 -0
  7. data/docs/business_phone_number/profile.md +220 -0
  8. data/docs/configuration.md +172 -0
  9. data/docs/errors.md +208 -0
  10. data/docs/media/README.md +161 -0
  11. data/docs/message_templates/README.md +206 -0
  12. data/docs/message_templates/authentication.md +107 -0
  13. data/docs/message_templates/carousel.md +103 -0
  14. data/docs/message_templates/components.md +179 -0
  15. data/docs/message_templates/library.md +98 -0
  16. data/docs/message_templates/limited_time_offer.md +94 -0
  17. data/docs/message_templates/responses.md +173 -0
  18. data/docs/message_templates/standard.md +138 -0
  19. data/docs/messages/README.md +135 -0
  20. data/docs/messages/address.md +86 -0
  21. data/docs/messages/audio.md +43 -0
  22. data/docs/messages/contacts.md +129 -0
  23. data/docs/messages/document.md +49 -0
  24. data/docs/messages/image.md +59 -0
  25. data/docs/messages/interactive.md +281 -0
  26. data/docs/messages/location.md +51 -0
  27. data/docs/messages/location_request.md +59 -0
  28. data/docs/messages/mark_message_as_read.md +84 -0
  29. data/docs/messages/reaction.md +59 -0
  30. data/docs/messages/sticker.md +40 -0
  31. data/docs/messages/template.md +166 -0
  32. data/docs/messages/text.md +55 -0
  33. data/docs/messages/video.md +41 -0
  34. data/docs/subscribed_app/README.md +139 -0
  35. data/docs/webhooks/README.md +209 -0
  36. data/docs/webhooks/account_alerts.md +44 -0
  37. data/docs/webhooks/account_review_update.md +29 -0
  38. data/docs/webhooks/account_update.md +54 -0
  39. data/docs/webhooks/automatic_events.md +47 -0
  40. data/docs/webhooks/business_capability_update.md +40 -0
  41. data/docs/webhooks/history.md +44 -0
  42. data/docs/webhooks/message_template_components_update.md +46 -0
  43. data/docs/webhooks/message_template_quality_update.md +49 -0
  44. data/docs/webhooks/message_template_status_update.md +55 -0
  45. data/docs/webhooks/messages.md +385 -0
  46. data/docs/webhooks/partner_solutions.md +35 -0
  47. data/docs/webhooks/payment_configuration_update.md +40 -0
  48. data/docs/webhooks/phone_number_name_update.md +44 -0
  49. data/docs/webhooks/phone_number_quality_update.md +40 -0
  50. data/docs/webhooks/security.md +43 -0
  51. data/docs/webhooks/smb_app_state_sync.md +53 -0
  52. data/docs/webhooks/smb_message_echoes.md +54 -0
  53. data/docs/webhooks/template_category_update.md +52 -0
  54. data/docs/webhooks/user_preferences.md +48 -0
  55. data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
  56. data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
  57. data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
  58. data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
  59. data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
  60. data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
  61. data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
  62. data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
  63. data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
  64. data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
  65. data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
  66. data/lib/ruby/whatsapp/business_phone_number/transport.rb +11 -12
  67. data/lib/ruby/whatsapp/business_phone_number.rb +11 -4
  68. data/lib/ruby/whatsapp/message_templates.rb +2 -6
  69. data/lib/ruby/whatsapp/path_building.rb +33 -0
  70. data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
  71. data/lib/ruby/whatsapp/version.rb +1 -1
  72. 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