ruby-whatsapp 0.4.0 → 0.6.1
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 +152 -0
- data/README.md +322 -617
- data/docs/README.md +76 -0
- data/docs/business_phone_number/README.md +243 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/contact_book.md +90 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/business_phone_number/username.md +157 -0
- data/docs/configuration.md +177 -0
- data/docs/errors.md +219 -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 +215 -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 +212 -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 +342 -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 +193 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/schedule/README.md +183 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +217 -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/business_username_updates.md +55 -0
- data/docs/webhooks/history.md +107 -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 +520 -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 +91 -0
- data/docs/webhooks/smb_message_echoes.md +91 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_id_update.md +84 -0
- data/docs/webhooks/user_preferences.md +87 -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/contact_book/delete.rb +87 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -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 +12 -12
- data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
- data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
- data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
- data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
- data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
- data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
- data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
- data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
- data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/messages/base.rb +47 -5
- data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
- data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
- data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/schedule/create.rb +101 -0
- data/lib/ruby/whatsapp/schedule/list.rb +50 -0
- data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
- data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
- data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
- data/lib/ruby/whatsapp/schedule/response.rb +18 -0
- data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
- data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
- data/lib/ruby/whatsapp/schedule.rb +15 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
- data/lib/ruby/whatsapp/webhook/change.rb +3 -1
- data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
- data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
- data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
- data/lib/ruby/whatsapp/webhook/history.rb +73 -34
- data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
- data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
- data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
- data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
- data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
- data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
- data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
- data/lib/ruby/whatsapp/webhook/message.rb +3 -0
- data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
- data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
- data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
- data/lib/ruby/whatsapp/webhook/status.rb +23 -2
- data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
- data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
- data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
- data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
- data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
- metadata +100 -1
|
@@ -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,91 @@
|
|
|
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: **high**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "smb_app_state_sync",
|
|
12
|
+
"value": {
|
|
13
|
+
"messaging_product": "whatsapp",
|
|
14
|
+
"metadata": { "display_phone_number": "15550783881", "phone_number_id": "106540352242922" },
|
|
15
|
+
"state_sync": [
|
|
16
|
+
{ "type": "contact",
|
|
17
|
+
"contact": { "full_name": "Pablo Morales",
|
|
18
|
+
"first_name": "Pablo",
|
|
19
|
+
"phone_number": "16505551234",
|
|
20
|
+
"user_id": "US.13491208655302741918",
|
|
21
|
+
"username": "pablomorales" },
|
|
22
|
+
"action": "add",
|
|
23
|
+
"metadata": { "timestamp": "1750030073" } }
|
|
24
|
+
] } }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Accessors
|
|
28
|
+
|
|
29
|
+
| Accessor | Type |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `messaging_product` | `"whatsapp"` |
|
|
32
|
+
| `metadata` | `Metadata` — the business phone number whose address book this is |
|
|
33
|
+
| `state_sync` | `Array<SmbAppStateSync::StateSync>` |
|
|
34
|
+
| `raw` | The untouched `value` hash |
|
|
35
|
+
|
|
36
|
+
### `StateSync`
|
|
37
|
+
|
|
38
|
+
| Accessor | Meaning |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `type` | `"contact"` is the only documented kind |
|
|
41
|
+
| `action` | e.g. `add`, `update`, `remove` |
|
|
42
|
+
| `contact` | An `SmbAppStateSync::Contact`, or `nil` |
|
|
43
|
+
| `timestamp` | Flattened up from the entry's own `metadata.timestamp` |
|
|
44
|
+
|
|
45
|
+
The entry's `metadata` is **not** the value-level `Metadata` — it carries only a
|
|
46
|
+
timestamp, so the wrapper is dropped rather than typed.
|
|
47
|
+
|
|
48
|
+
### `Contact`
|
|
49
|
+
|
|
50
|
+
| Accessor | Meaning |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `full_name` | |
|
|
53
|
+
| `first_name` | |
|
|
54
|
+
| `phone_number` | Omitted once the contact adopts a username |
|
|
55
|
+
| `user_id` | Their [BSUID](messages.md#business-scoped-user-ids-bsuid) |
|
|
56
|
+
| `parent_user_id` | Their parent BSUID, present only if enrolled |
|
|
57
|
+
| `username` | Set only if adopted. Read **flat** here, unlike `Contact#username` on `messages`, which Meta nests under `profile` |
|
|
58
|
+
|
|
59
|
+
This is the gem's third `Contact`, and none are interchangeable:
|
|
60
|
+
[`Webhook::Contact`](messages.md#value-accessors) is a notification's sender identity,
|
|
61
|
+
[`Message::Contacts::Contact`](messages.md#contacts) is a shared contact *card*, and this
|
|
62
|
+
is a device address-book row.
|
|
63
|
+
|
|
64
|
+
## Handling it
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
when "smb_app_state_sync"
|
|
68
|
+
change.value.state_sync.each do |sync|
|
|
69
|
+
next unless sync.type == "contact"
|
|
70
|
+
|
|
71
|
+
case sync.action
|
|
72
|
+
when "add", "update"
|
|
73
|
+
Contact.upsert_from_meta(bsuid: sync.contact.user_id, phone: sync.contact.phone_number,
|
|
74
|
+
name: sync.contact.full_name, username: sync.contact.username)
|
|
75
|
+
when "remove"
|
|
76
|
+
Contact.find_by(bsuid: sync.contact.user_id)&.destroy
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> **Changed in 0.6.0.** `StateSync#contact` used to be a raw Hash, because Meta had
|
|
82
|
+
> published no shape for it. It is now a typed `Contact`, so `sync.contact["phone_number"]`
|
|
83
|
+
> becomes `sync.contact.phone_number`, and an entry carrying no contact returns `nil`
|
|
84
|
+
> rather than `{}`.
|
|
85
|
+
|
|
86
|
+
> The `state_sync` key itself is still inferred from the field name rather than confirmed
|
|
87
|
+
> by a published example — hence `raw`, so a drifted key leaves a trace.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|
|
@@ -0,0 +1,91 @@
|
|
|
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. Messages the business *revoked* or *edited* from the app
|
|
6
|
+
arrive here too.
|
|
7
|
+
|
|
8
|
+
`Whatsapp::Webhook::SmbMessageEchoes` · confidence: **high**
|
|
9
|
+
|
|
10
|
+
## Payload
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "field": "smb_message_echoes",
|
|
14
|
+
"value": {
|
|
15
|
+
"messaging_product": "whatsapp",
|
|
16
|
+
"metadata": { "display_phone_number": "15550783881", "phone_number_id": "106540352242922" },
|
|
17
|
+
"contacts": [
|
|
18
|
+
{ "profile": { "username": "pablomorales" },
|
|
19
|
+
"wa_id": "16505551234",
|
|
20
|
+
"user_id": "US.13491208655302741918" }
|
|
21
|
+
],
|
|
22
|
+
"message_echoes": [
|
|
23
|
+
{ "from": "15550783881",
|
|
24
|
+
"to": "16505551234",
|
|
25
|
+
"to_user_id": "US.13491208655302741918",
|
|
26
|
+
"id": "wamid.HBg",
|
|
27
|
+
"timestamp": "1750030073",
|
|
28
|
+
"type": "text",
|
|
29
|
+
"text": { "body": "Thanks for shopping with us!" } }
|
|
30
|
+
] } }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Accessors
|
|
34
|
+
|
|
35
|
+
| Accessor | Type |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `messaging_product` | `"whatsapp"` |
|
|
38
|
+
| `metadata` | `Metadata` — `display_phone_number`, `phone_number_id` |
|
|
39
|
+
| `contacts` | `Array<Contact>` — the customers these went to |
|
|
40
|
+
| `message_echoes` | `Array<SmbMessageEchoes::Echo>` |
|
|
41
|
+
| `raw` | The untouched `value` hash |
|
|
42
|
+
|
|
43
|
+
### `Echo`
|
|
44
|
+
|
|
45
|
+
| Accessor | Meaning |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `to` | The customer's phone number, or `nil` once they adopt a username |
|
|
48
|
+
| `to_user_id` | Their [BSUID](messages.md#business-scoped-user-ids-bsuid) |
|
|
49
|
+
| `to_parent_user_id` | Their parent BSUID, present only if enrolled |
|
|
50
|
+
| `message` | The echoed `Message::*`, dispatched by `type` |
|
|
51
|
+
|
|
52
|
+
`Echo#message` goes through the same `Message.deserialize` dispatcher as
|
|
53
|
+
[`messages`](messages.md), so an echo is a fully typed `Message::Text`, `Message::Image`,
|
|
54
|
+
`Message::Revoke`, `Message::Edit`, and so on — with the same accessors.
|
|
55
|
+
|
|
56
|
+
Why a wrapper rather than three more accessors on `Message::Base`? Because an *inbound*
|
|
57
|
+
message can never carry a recipient. Putting `to`/`to_user_id`/`to_parent_user_id` on the
|
|
58
|
+
envelope shared by every message type would hand fifteen classes three permanently-`nil`
|
|
59
|
+
readers, and would turn the `NoMethodError` that currently tells you `message.to` is
|
|
60
|
+
meaningless into a silent `nil`.
|
|
61
|
+
|
|
62
|
+
## Handling it
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
when "smb_message_echoes"
|
|
66
|
+
change.value.message_echoes.each do |echo|
|
|
67
|
+
# The customer may have no phone number; the BSUID always identifies them
|
|
68
|
+
conversation = Conversation.for(bsuid: echo.to_user_id, wa_id: echo.to)
|
|
69
|
+
|
|
70
|
+
conversation.messages.create!(
|
|
71
|
+
wamid: echo.message.id,
|
|
72
|
+
direction: :outbound,
|
|
73
|
+
source: :business_app,
|
|
74
|
+
body: echo.message.try(:body)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Note `echo.message.from` is *your* business number, not the customer's — these are
|
|
80
|
+
messages you sent. The customer is on `echo.to*`.
|
|
81
|
+
|
|
82
|
+
> **Changed in 0.6.0.** `message_echoes` used to be an `Array<Message::*>` directly;
|
|
83
|
+
> each entry is now an `Echo` wrapping that message, so `echo.body` becomes
|
|
84
|
+
> `echo.message.body`. The wrapper is what carries the new recipient identifiers.
|
|
85
|
+
|
|
86
|
+
> The `message_echoes` key itself is still inferred from the field name rather than
|
|
87
|
+
> confirmed by a published example — hence `raw`, so a drifted key leaves a trace.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# `template_category_update`
|
|
2
|
+
|
|
3
|
+
Meta reclassified a template — most often moving something you filed as `UTILITY` into
|
|
4
|
+
`MARKETING`. This matters because category determines **how you are billed** and
|
|
5
|
+
whether the message is subject to marketing opt-outs.
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::TemplateCategoryUpdate` · confidence: **moderate-high**
|
|
8
|
+
|
|
9
|
+
## Payload
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "field": "template_category_update",
|
|
13
|
+
"value": {
|
|
14
|
+
"message_template_id": "123",
|
|
15
|
+
"message_template_name": "order_confirmation",
|
|
16
|
+
"message_template_language": "en_US",
|
|
17
|
+
"previous_category": "MARKETING",
|
|
18
|
+
"new_category": "UTILITY",
|
|
19
|
+
"correct_category": "UTILITY"
|
|
20
|
+
} }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Accessors
|
|
24
|
+
|
|
25
|
+
| Accessor | Meaning |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `message_template_id` | The template's ID |
|
|
28
|
+
| `message_template_name` | Its name |
|
|
29
|
+
| `message_template_language` | Its locale |
|
|
30
|
+
| `previous_category` | What it was |
|
|
31
|
+
| `new_category` | What it is now |
|
|
32
|
+
| `correct_category` | What Meta believes it should be |
|
|
33
|
+
|
|
34
|
+
## Handling it
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
when "template_category_update"
|
|
38
|
+
update = change.value
|
|
39
|
+
|
|
40
|
+
MessageTemplate
|
|
41
|
+
.find_by(meta_id: update.message_template_id)
|
|
42
|
+
&.update!(category: update.new_category)
|
|
43
|
+
|
|
44
|
+
Billing.recheck!(update.message_template_name) if update.new_category == "MARKETING"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Best-effort schema.** Meta publishes no JSON example for this field. Validate
|
|
48
|
+
> against a real payload before depending on it in production.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# `user_id_update`
|
|
2
|
+
|
|
3
|
+
A WhatsApp user's business-scoped user ID (BSUID) changed — most commonly because they
|
|
4
|
+
changed their phone number (see the [`system` message type's `user_changed_user_id`
|
|
5
|
+
event](messages.md#system) for the same event surfaced on the `messages` field).
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::UserIdUpdate` · confidence: **confirmed** — Meta publishes a real
|
|
8
|
+
JSON example
|
|
9
|
+
|
|
10
|
+
## Payload
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "field": "user_id_update",
|
|
14
|
+
"value": {
|
|
15
|
+
"messaging_product": "whatsapp",
|
|
16
|
+
"metadata": { "display_phone_number": "15550783881", "phone_number_id": "106540352242922" },
|
|
17
|
+
"contacts": [{ "profile": { "name": "Jane Doe" }, "wa_id": "16505551234" }],
|
|
18
|
+
"user_id_update": [
|
|
19
|
+
{
|
|
20
|
+
"wa_id": "16505551234",
|
|
21
|
+
"detail": "User id for Jane Doe has been updated.",
|
|
22
|
+
"user_id": { "previous": "US.OLD", "current": "US.NEW" },
|
|
23
|
+
"parent_user_id": { "previous": "US.ENT.OLD", "current": "US.ENT.NEW" },
|
|
24
|
+
"timestamp": "1750030073"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
} }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`parent_user_id` is present only if you have enrolled in [parent
|
|
31
|
+
BSUIDs](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids#parent-business-scoped-user-ids).
|
|
32
|
+
|
|
33
|
+
## Accessors
|
|
34
|
+
|
|
35
|
+
| Accessor | Meaning |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `messaging_product` | `"whatsapp"` |
|
|
38
|
+
| `metadata` | `Metadata` — `display_phone_number`, `phone_number_id` |
|
|
39
|
+
| `contacts` | `Array<Contact>` — the [same BSUID-aware `Contact`](messages.md#business-scoped-user-ids-bsuid) used by `messages` |
|
|
40
|
+
| `updates` | `Array<UserIdUpdate::Entry>` |
|
|
41
|
+
|
|
42
|
+
### `Entry`
|
|
43
|
+
|
|
44
|
+
| Accessor | Meaning |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `wa_id` | The user's phone number, if available |
|
|
47
|
+
| `detail` | Human-readable description of the change |
|
|
48
|
+
| `user_id` | `UserIdUpdate::IdChange` or `nil` — the user's previous and current BSUID |
|
|
49
|
+
| `parent_user_id` | `UserIdUpdate::IdChange` or `nil` — previous and current parent BSUID |
|
|
50
|
+
| `timestamp` | Unix seconds, as a String |
|
|
51
|
+
|
|
52
|
+
### `IdChange`
|
|
53
|
+
|
|
54
|
+
`Whatsapp::Webhook::UserIdUpdate::IdChange` — a sibling of `Entry`, not nested inside it.
|
|
55
|
+
One class serves both `user_id` and `parent_user_id`, since they are the same shape, and
|
|
56
|
+
each is `nil` rather than an all-nil object when its key is absent.
|
|
57
|
+
|
|
58
|
+
| Accessor | Meaning |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `previous` | The BSUID before the change |
|
|
61
|
+
| `current` | The BSUID after the change |
|
|
62
|
+
|
|
63
|
+
## Handling it
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
when "user_id_update"
|
|
67
|
+
change.value.updates.each do |update|
|
|
68
|
+
next unless update.user_id # nil when the entry carries only a parent BSUID change
|
|
69
|
+
|
|
70
|
+
user = User.find_by(bsuid: update.user_id.previous)
|
|
71
|
+
next Rails.logger.warn("[whatsapp] unknown BSUID #{update.user_id.previous}") unless user
|
|
72
|
+
|
|
73
|
+
user.update!(bsuid: update.user_id.current)
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Report a miss rather than letting `&.update!` swallow it — a rotation you never applied
|
|
78
|
+
leaves you writing to a dead BSUID with no diagnostics. Log or alert; do not `raise`, or
|
|
79
|
+
Meta will retry a request that can never succeed. `change.value.raw` carries the untouched
|
|
80
|
+
payload if you need to see what actually arrived.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids#user_id_update-webhooks>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# `user_preferences`
|
|
2
|
+
|
|
3
|
+
A customer changed their marketing preferences — most importantly, opting **out**.
|
|
4
|
+
Respecting this is not optional: continuing to send marketing to someone who stopped
|
|
5
|
+
you damages your quality rating and breaches Meta's policy.
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::UserPreferences` · confidence: **confirmed**
|
|
8
|
+
|
|
9
|
+
## Payload
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "field": "user_preferences",
|
|
13
|
+
"value": {
|
|
14
|
+
"messaging_product": "whatsapp",
|
|
15
|
+
"metadata": { "display_phone_number": "15550783881", "phone_number_id": "106540352242922" },
|
|
16
|
+
"contacts": [
|
|
17
|
+
{ "profile": { "name": "Pablo M.", "username": "pablomorales" },
|
|
18
|
+
"wa_id": "16505551234",
|
|
19
|
+
"user_id": "US.13491208655302741918" }
|
|
20
|
+
],
|
|
21
|
+
"user_preferences": [
|
|
22
|
+
{ "wa_id": "16505551234",
|
|
23
|
+
"user_id": "US.13491208655302741918",
|
|
24
|
+
"detail": "User requested to stop marketing messages",
|
|
25
|
+
"category": "marketing_messages",
|
|
26
|
+
"value": "stop",
|
|
27
|
+
"timestamp": 1750030073 }
|
|
28
|
+
] } }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Note the nesting: the entries sit under a repeat of the field's own name, the same shape
|
|
32
|
+
[`user_id_update`](user_id_update.md) uses.
|
|
33
|
+
|
|
34
|
+
## Value accessors
|
|
35
|
+
|
|
36
|
+
| Accessor | Type |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `messaging_product` | `String` — always `"whatsapp"` |
|
|
39
|
+
| `metadata` | `Metadata` — the business phone number this arrived on |
|
|
40
|
+
| `contacts` | `Array<Contact>` — `profile_name`, `username`, `wa_id`, `user_id`, `parent_user_id` |
|
|
41
|
+
| `preferences` | `Array<UserPreferences::Entry>` |
|
|
42
|
+
| `raw` | The untouched `value` hash |
|
|
43
|
+
|
|
44
|
+
## Entry accessors
|
|
45
|
+
|
|
46
|
+
| Accessor | Meaning |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `wa_id` | The customer's phone number, or `nil` once they adopt a username |
|
|
49
|
+
| `user_id` | Their [BSUID](messages.md#business-scoped-user-ids-bsuid) — the identifier that is always there |
|
|
50
|
+
| `parent_user_id` | Their parent BSUID, present only if enrolled |
|
|
51
|
+
| `detail` | Human-readable description |
|
|
52
|
+
| `category` | `"marketing_messages"` is the only documented value |
|
|
53
|
+
| `value` | `"stop"` or `"resume"` |
|
|
54
|
+
| `timestamp` | Unix seconds. Meta sends a **number** here, unlike the String timestamps on `messages` |
|
|
55
|
+
|
|
56
|
+
> `Entry#value` sits inside the change's own `value` hash, so the full path reads
|
|
57
|
+
> `change.value.preferences.first.value`. Meta's naming, not ours.
|
|
58
|
+
|
|
59
|
+
## Handling it
|
|
60
|
+
|
|
61
|
+
An opt-out from a username adopter may carry no phone number at all, so key off the
|
|
62
|
+
BSUID and fall back to `wa_id` — not the other way round:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
when "user_preferences"
|
|
66
|
+
change.value.preferences.each do |preference|
|
|
67
|
+
customer =
|
|
68
|
+
Customer.find_by(bsuid: preference.user_id) ||
|
|
69
|
+
Customer.find_by(wa_id: preference.wa_id)
|
|
70
|
+
|
|
71
|
+
customer&.update!(
|
|
72
|
+
marketing_opt_in: preference.value != "stop",
|
|
73
|
+
marketing_opt_in_changed_at: Time.at(preference.timestamp.to_i)
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Gate every `MARKETING`-category [template send](../messages/template.md) on this flag.
|
|
79
|
+
`UTILITY` and `AUTHENTICATION` templates are unaffected.
|
|
80
|
+
|
|
81
|
+
> **Changed in 0.6.0.** This field used to expose `wa_id`/`detail`/`value`/`timestamp`
|
|
82
|
+
> directly on `change.value`. Meta nests them one level deeper than the gem assumed, so
|
|
83
|
+
> those accessors could never return anything but `nil`; they are now on `Entry`.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|