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,209 @@
|
|
|
1
|
+
# Webhooks
|
|
2
|
+
|
|
3
|
+
Meta pushes inbound messages, delivery statuses, and ~18 other account and template
|
|
4
|
+
notifications to a callback URL you register. This directory covers the *inbound*
|
|
5
|
+
direction — the opposite of everything under [../messages/](../messages/README.md).
|
|
6
|
+
|
|
7
|
+
> **Two different things.** `Whatsapp::Webhook` deserializes notifications once Meta is
|
|
8
|
+
> already sending them. Turning that delivery **on** for a WhatsApp Business Account is
|
|
9
|
+
> [`Whatsapp::SubscribedApp`](../subscribed_app/README.md). You need both.
|
|
10
|
+
|
|
11
|
+
## Install (Rails)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle exec rake whatsapp:install:webhook
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This copies a personalizable controller to
|
|
18
|
+
`app/controllers/whatsapp/webhooks_controller.rb` — it never overwrites an existing
|
|
19
|
+
one — and prints the routes and configuration you still need to add by hand:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# config/routes.rb
|
|
23
|
+
get "/whatsapp/webhooks", to: "whatsapp/webhooks#verify"
|
|
24
|
+
post "/whatsapp/webhooks", to: "whatsapp/webhooks#receive"
|
|
25
|
+
|
|
26
|
+
# config/initializers/whatsapp.rb
|
|
27
|
+
Whatsapp.configure do |config|
|
|
28
|
+
config.verify_token = Rails.application.credentials.whatsapp_verify_token
|
|
29
|
+
config.app_secret = Rails.application.credentials.whatsapp_app_secret
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The rake task is registered by `Whatsapp::Railtie` and only exists inside a Rails app.
|
|
34
|
+
Outside Rails, call the installer directly — it is plain Ruby with no Rails dependency:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Whatsapp::Webhook::Installer.call(root: Dir.pwd) # => :created | :skipped
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## The generated controller
|
|
41
|
+
|
|
42
|
+
Yours to edit. It handles the handshake and the signature check, then deserializes
|
|
43
|
+
every notification into typed objects and leaves a `# TODO` where your handling goes:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class Whatsapp::WebhooksController < ApplicationController
|
|
47
|
+
skip_before_action :verify_authenticity_token, raise: false
|
|
48
|
+
|
|
49
|
+
def verify
|
|
50
|
+
challenge = Whatsapp::Webhook::Verification.call(params: params)
|
|
51
|
+
challenge ? render(plain: challenge) : head(:forbidden)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def receive
|
|
55
|
+
raw_body = request.body.read
|
|
56
|
+
return head(:unauthorized) unless Whatsapp::Webhook::Signature.valid?(
|
|
57
|
+
payload: raw_body, header: request.headers["X-Hub-Signature-256"]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
notification = Whatsapp::Webhook::Notification.deserialize(JSON.parse(raw_body))
|
|
61
|
+
# notification.entry.each { |entry| entry.changes.each { |change| WebhookJob.perform_later(change) } }
|
|
62
|
+
|
|
63
|
+
head :ok
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **Answer fast, work later.** Meta retries a notification that doesn't get a 2xx
|
|
69
|
+
> quickly. Enqueue the work; don't do it inline.
|
|
70
|
+
|
|
71
|
+
## Verification (the GET handshake)
|
|
72
|
+
|
|
73
|
+
When you register the callback URL, Meta calls it once with `hub.mode`,
|
|
74
|
+
`hub.verify_token`, and `hub.challenge`. Echo the challenge back if the token matches:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Whatsapp::Webhook::Verification.call(params: params)
|
|
78
|
+
# => "1158201444" — the challenge, to render as plain text
|
|
79
|
+
# => nil — mode wasn't "subscribe", or the token didn't match
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The token comparison uses `ActiveSupport::SecurityUtils.secure_compare`. A nil or empty
|
|
83
|
+
configured `verify_token` always returns `nil` rather than accepting anything.
|
|
84
|
+
|
|
85
|
+
## Signature (the POST check)
|
|
86
|
+
|
|
87
|
+
Every notification carries an `X-Hub-Signature-256` header: HMAC-SHA256 of the **raw
|
|
88
|
+
request body** using your app secret.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Whatsapp::Webhook::Signature.valid?(payload: raw_body, header: request.headers["X-Hub-Signature-256"])
|
|
92
|
+
# => true | false
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Use the raw body, not `params`.** Re-serializing a parsed hash changes the bytes
|
|
96
|
+
> and the signature will never match.
|
|
97
|
+
|
|
98
|
+
Returns `false` — never raises — for a nil payload, a missing or empty header, or a
|
|
99
|
+
missing app secret.
|
|
100
|
+
|
|
101
|
+
## Multi-tenant apps
|
|
102
|
+
|
|
103
|
+
Both take the credential as an overridable keyword, defaulting to
|
|
104
|
+
`Whatsapp.configuration`. That override is the multi-tenant path: an app serving many
|
|
105
|
+
customers, each with their own Meta App, resolves the account first and passes its
|
|
106
|
+
credentials explicitly.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
account = Account.find_by!(slug: params[:account_slug])
|
|
110
|
+
|
|
111
|
+
Whatsapp::Webhook::Verification.call(params:, verify_token: account.verify_token)
|
|
112
|
+
Whatsapp::Webhook::Signature.valid?(payload: raw_body, header:, app_secret: account.app_secret)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## The notification tree
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
{ object, entry: [ { id, changes: [ { field, value } ] } ] }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
notification = Whatsapp::Webhook::Notification.deserialize(JSON.parse(raw_body))
|
|
123
|
+
|
|
124
|
+
notification.object # => "whatsapp_business_account"
|
|
125
|
+
notification.entry.first.id # => "102290129340398" — the WABA ID
|
|
126
|
+
change = notification.entry.first.changes.first
|
|
127
|
+
change.field # => "messages"
|
|
128
|
+
change.value # => #<Whatsapp::Webhook::Messages ...>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`change.field` is one of Meta's 19 documented field names; `change.value` is that
|
|
132
|
+
field's deserialized payload, resolved through a frozen `Change::FIELDS` registry —
|
|
133
|
+
**never `const_get` on the field name**, since it comes straight off the internet.
|
|
134
|
+
|
|
135
|
+
A field not in the registry (something Meta adds after this gem ships) falls back to
|
|
136
|
+
`UnknownField` rather than raising, so your controller never 500s on an unrecognized
|
|
137
|
+
notification:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
change.value # => #<Whatsapp::Webhook::UnknownField ...>
|
|
141
|
+
change.value["some_key"] # reach into the raw hash
|
|
142
|
+
change.value.to_h # the whole thing
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## The 19 fields
|
|
146
|
+
|
|
147
|
+
| Field | Page | Confidence |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| `messages` | [messages.md](messages.md) | **Confirmed** — Meta publishes a real payload |
|
|
150
|
+
| `account_alerts` | [account_alerts.md](account_alerts.md) | Moderate |
|
|
151
|
+
| `account_review_update` | [account_review_update.md](account_review_update.md) | High |
|
|
152
|
+
| `account_update` | [account_update.md](account_update.md) | Moderate |
|
|
153
|
+
| `automatic_events` | [automatic_events.md](automatic_events.md) | Low |
|
|
154
|
+
| `business_capability_update` | [business_capability_update.md](business_capability_update.md) | Moderate |
|
|
155
|
+
| `history` | [history.md](history.md) | Low |
|
|
156
|
+
| `message_template_components_update` | [message_template_components_update.md](message_template_components_update.md) | Moderate |
|
|
157
|
+
| `message_template_quality_update` | [message_template_quality_update.md](message_template_quality_update.md) | Moderate-high |
|
|
158
|
+
| `message_template_status_update` | [message_template_status_update.md](message_template_status_update.md) | High |
|
|
159
|
+
| `partner_solutions` | [partner_solutions.md](partner_solutions.md) | Low |
|
|
160
|
+
| `payment_configuration_update` | [payment_configuration_update.md](payment_configuration_update.md) | Moderate |
|
|
161
|
+
| `phone_number_name_update` | [phone_number_name_update.md](phone_number_name_update.md) | Moderate-high |
|
|
162
|
+
| `phone_number_quality_update` | [phone_number_quality_update.md](phone_number_quality_update.md) | Moderate |
|
|
163
|
+
| `security` | [security.md](security.md) | Low |
|
|
164
|
+
| `smb_app_state_sync` | [smb_app_state_sync.md](smb_app_state_sync.md) | Low-moderate |
|
|
165
|
+
| `smb_message_echoes` | [smb_message_echoes.md](smb_message_echoes.md) | Moderate |
|
|
166
|
+
| `template_category_update` | [template_category_update.md](template_category_update.md) | Moderate-high |
|
|
167
|
+
| `user_preferences` | [user_preferences.md](user_preferences.md) | Moderate |
|
|
168
|
+
|
|
169
|
+
> **On confidence.** Only `messages` has a Meta-published JSON example. The other 18
|
|
170
|
+
> field classes are reconstructed from the one-line descriptions in Meta's docs plus
|
|
171
|
+
> general Cloud API knowledge. They are real, tested classes — but validate one against
|
|
172
|
+
> a real payload (App Dashboard → "send test payload") before depending on it in
|
|
173
|
+
> production.
|
|
174
|
+
|
|
175
|
+
## Dispatching
|
|
176
|
+
|
|
177
|
+
`Change#field` is a plain String, which makes a `case` the natural handler:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
notification.entry.each do |entry|
|
|
181
|
+
entry.changes.each do |change|
|
|
182
|
+
case change.field
|
|
183
|
+
when "messages"
|
|
184
|
+
change.value.messages.each { |m| handle_message(m) }
|
|
185
|
+
change.value.statuses.each { |s| handle_status(s) }
|
|
186
|
+
when "message_template_status_update"
|
|
187
|
+
Template.find_by(meta_id: change.value.message_template_id)
|
|
188
|
+
&.update!(status: change.value.event)
|
|
189
|
+
else
|
|
190
|
+
Rails.logger.info("Unhandled webhook field: #{change.field}")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Conventions
|
|
197
|
+
|
|
198
|
+
Every class here follows the read-side pattern — no validations, no `serialize`, just
|
|
199
|
+
a class-level `.deserialize(data)`:
|
|
200
|
+
|
|
201
|
+
1. Plain Ruby object, `attr_accessor` per field.
|
|
202
|
+
2. `.deserialize(data)` tolerates a nil or partial hash (`data ||= {}`) and missing
|
|
203
|
+
arrays (`Array(data["key"]).map { ... }`).
|
|
204
|
+
3. Optional nested objects return `nil` when their source key is absent, not an
|
|
205
|
+
all-nil instance — e.g. `Message::Context.deserialize(nil) # => nil`.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# `account_alerts`
|
|
2
|
+
|
|
3
|
+
Operational alerts about the account — messaging-limit changes, policy notices,
|
|
4
|
+
capability warnings.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::AccountAlerts` · confidence: **moderate**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "account_alerts",
|
|
12
|
+
"value": {
|
|
13
|
+
"entity_type": "WABA",
|
|
14
|
+
"entity_id": "102290129340398",
|
|
15
|
+
"alert_severity": "INFO",
|
|
16
|
+
"alert_status": "ACTIVE",
|
|
17
|
+
"alert_description": "Messaging limit increased"
|
|
18
|
+
} }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Accessors
|
|
22
|
+
|
|
23
|
+
| Accessor | Meaning |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `entity_type` | What the alert is about, e.g. `WABA` |
|
|
26
|
+
| `entity_id` | That entity's ID |
|
|
27
|
+
| `alert_severity` | e.g. `INFO`, `WARNING`, `CRITICAL` |
|
|
28
|
+
| `alert_status` | e.g. `ACTIVE`, `RESOLVED` |
|
|
29
|
+
| `alert_description` | Human-readable text |
|
|
30
|
+
|
|
31
|
+
## Handling it
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
when "account_alerts"
|
|
35
|
+
alert = change.value
|
|
36
|
+
Ops.notify(alert.alert_description) if alert.alert_severity != "INFO"
|
|
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,29 @@
|
|
|
1
|
+
# `account_review_update`
|
|
2
|
+
|
|
3
|
+
The verdict on a WhatsApp Business Account review.
|
|
4
|
+
|
|
5
|
+
`Whatsapp::Webhook::AccountReviewUpdate` · confidence: **high** — one field, and Meta
|
|
6
|
+
documents it plainly
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "account_review_update", "value": { "decision": "APPROVED" } }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Accessors
|
|
15
|
+
|
|
16
|
+
| Accessor | Meaning |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `decision` | `APPROVED` or `REJECTED` |
|
|
19
|
+
|
|
20
|
+
## Handling it
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
when "account_review_update"
|
|
24
|
+
Account.find_by(waba_id: entry.id)&.update!(review_decision: change.value.decision)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# `account_update`
|
|
2
|
+
|
|
3
|
+
Something changed about the WhatsApp Business Account itself — most importantly, a ban
|
|
4
|
+
or a restriction. If you handle exactly one non-`messages` field, make it this one:
|
|
5
|
+
a banned WABA stops delivering everything.
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::AccountUpdate` · confidence: **moderate**
|
|
8
|
+
|
|
9
|
+
## Payload
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "field": "account_update",
|
|
13
|
+
"value": {
|
|
14
|
+
"phone_number": "15550783881",
|
|
15
|
+
"event": "DISABLED_UPDATE",
|
|
16
|
+
"ban_info": { "waba_ban_state": "DISABLE", "waba_ban_date": "2024-01-01" }
|
|
17
|
+
} }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Accessors
|
|
21
|
+
|
|
22
|
+
| Accessor | Meaning |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `phone_number` | The number the event concerns |
|
|
25
|
+
| `event` | e.g. `DISABLED_UPDATE`, `VERIFIED_ACCOUNT`, `ACCOUNT_RESTRICTION` |
|
|
26
|
+
| `ban_info` | `AccountUpdate::BanInfo` or `nil` |
|
|
27
|
+
|
|
28
|
+
### `BanInfo`
|
|
29
|
+
|
|
30
|
+
| Accessor | Meaning |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `waba_ban_state` | e.g. `DISABLE`, `WARN`, `REINSTATE` |
|
|
33
|
+
| `waba_ban_date` | When it takes effect |
|
|
34
|
+
|
|
35
|
+
## Handling it
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
when "account_update"
|
|
39
|
+
update = change.value
|
|
40
|
+
|
|
41
|
+
if update.ban_info
|
|
42
|
+
Ops.page!(
|
|
43
|
+
"WABA #{entry.id} ban state #{update.ban_info.waba_ban_state} " \
|
|
44
|
+
"effective #{update.ban_info.waba_ban_date}"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Best-effort schema.** Meta publishes no JSON example for this field. Validate
|
|
50
|
+
> against a real payload before depending on it in production.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# `automatic_events`
|
|
2
|
+
|
|
3
|
+
Conversion and engagement events Meta attributes to a message — a purchase, a signup —
|
|
4
|
+
used for ad measurement.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::AutomaticEvents` · confidence: **low**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "automatic_events",
|
|
12
|
+
"value": { "event_type": "purchase",
|
|
13
|
+
"message_id": "wamid.HBg...",
|
|
14
|
+
"event_data": { "value": 19.99, "currency": "USD" } } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Accessors
|
|
18
|
+
|
|
19
|
+
| Accessor | Meaning |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `event_type` | e.g. `purchase` |
|
|
22
|
+
| `message_id` | The WAMID the event is attributed to |
|
|
23
|
+
| `event_data` | **Raw Hash**, defaults to `{}` |
|
|
24
|
+
|
|
25
|
+
`event_data` is deliberately left as a raw hash: its keys vary by `event_type` and Meta
|
|
26
|
+
publishes no schema, so typing it would be guesswork.
|
|
27
|
+
|
|
28
|
+
## Handling it
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
when "automatic_events"
|
|
32
|
+
event = change.value
|
|
33
|
+
|
|
34
|
+
Conversion.create!(
|
|
35
|
+
kind: event.event_type,
|
|
36
|
+
wamid: event.message_id,
|
|
37
|
+
amount: event.event_data["value"],
|
|
38
|
+
currency: event.event_data["currency"]
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> **Best-effort schema, low confidence.** Validate against a real payload before
|
|
43
|
+
> depending on it in production.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `business_capability_update`
|
|
2
|
+
|
|
3
|
+
Your account's capability limits changed — how many conversations each number may
|
|
4
|
+
start per day, and how many numbers the business may hold. A limit increase usually
|
|
5
|
+
follows sustained good quality; a decrease follows the opposite.
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::BusinessCapabilityUpdate` · confidence: **moderate**
|
|
8
|
+
|
|
9
|
+
## Payload
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "field": "business_capability_update",
|
|
13
|
+
"value": { "max_daily_conversation_per_phone": 100000,
|
|
14
|
+
"max_phone_numbers_per_business": 20 } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Accessors
|
|
18
|
+
|
|
19
|
+
| Accessor | Meaning |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `max_daily_conversation_per_phone` | Business-initiated conversations per number per day |
|
|
22
|
+
| `max_phone_numbers_per_business` | Numbers this business may register |
|
|
23
|
+
|
|
24
|
+
## Handling it
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
when "business_capability_update"
|
|
28
|
+
caps = change.value
|
|
29
|
+
Throttle.update!(daily_cap: caps.max_daily_conversation_per_phone)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Useful for pacing an outbound campaign against the real ceiling rather than a
|
|
33
|
+
hard-coded guess.
|
|
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
|
+
# `history`
|
|
2
|
+
|
|
3
|
+
Conversation history synced from the WhatsApp Business app when a number moves onto
|
|
4
|
+
Cloud API. It arrives in chunks, so `phase`, `chunk_order`, and `progress` are how you
|
|
5
|
+
track completeness.
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::History` · confidence: **low**
|
|
8
|
+
|
|
9
|
+
## Payload
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "field": "history",
|
|
13
|
+
"value": { "metadata": { "phase": 1, "chunk_order": 1, "progress": 50 },
|
|
14
|
+
"threads": [{ "id": "thread.1" }] } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Accessors
|
|
18
|
+
|
|
19
|
+
| Accessor | Meaning |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `phase` | Sync phase (dug from `value.metadata`) |
|
|
22
|
+
| `chunk_order` | This chunk's position (dug from `value.metadata`) |
|
|
23
|
+
| `progress` | Percent complete, 0–100 (dug from `value.metadata`) |
|
|
24
|
+
| `threads` | **Raw Array** of thread hashes |
|
|
25
|
+
|
|
26
|
+
Note the three scalars are flattened up from the nested `metadata` object, while
|
|
27
|
+
`threads` is left raw — its element shape is undocumented.
|
|
28
|
+
|
|
29
|
+
## Handling it
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
when "history"
|
|
33
|
+
sync = change.value
|
|
34
|
+
|
|
35
|
+
HistoryChunk.create!(phase: sync.phase, order: sync.chunk_order, threads: sync.threads)
|
|
36
|
+
Importer.finish! if sync.progress == 100
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **Best-effort schema, low confidence.** Validate against a real payload before
|
|
40
|
+
> depending on it in production.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# `message_template_components_update`
|
|
2
|
+
|
|
3
|
+
A template's components changed — usually because Meta edited it, or because an edit
|
|
4
|
+
you submitted was applied.
|
|
5
|
+
|
|
6
|
+
`Whatsapp::Webhook::MessageTemplateComponentsUpdate` · confidence: **moderate**
|
|
7
|
+
|
|
8
|
+
## Payload
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{ "field": "message_template_components_update",
|
|
12
|
+
"value": {
|
|
13
|
+
"message_template_id": "123",
|
|
14
|
+
"message_template_name": "order_confirmation",
|
|
15
|
+
"message_template_language": "en_US",
|
|
16
|
+
"message_template_element": "BODY"
|
|
17
|
+
} }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Accessors
|
|
21
|
+
|
|
22
|
+
| Accessor | Meaning |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `message_template_id` | The template's ID |
|
|
25
|
+
| `message_template_name` | Its name |
|
|
26
|
+
| `message_template_language` | Its locale |
|
|
27
|
+
| `message_template_element` | Which component changed |
|
|
28
|
+
|
|
29
|
+
## Handling it
|
|
30
|
+
|
|
31
|
+
The notification says *that* something changed, not what it changed to. Re-read the
|
|
32
|
+
template to get the new shape:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
when "message_template_components_update"
|
|
36
|
+
node = Whatsapp::MessageTemplates.new.find(template_id: change.value.message_template_id)
|
|
37
|
+
MessageTemplate.find_by(meta_id: node.id)&.update!(components: node.components)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> **Best-effort schema.** Meta describes this field in one line and publishes no JSON
|
|
41
|
+
> example. Validate against a real payload (App Dashboard → "send test payload")
|
|
42
|
+
> before depending on it in production.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# `message_template_quality_update`
|
|
2
|
+
|
|
3
|
+
A template's quality score changed. Quality is computed from how recipients react —
|
|
4
|
+
blocks, reports, and "not useful" feedback push it down. A template that reaches `RED`
|
|
5
|
+
gets paused, then eventually disabled, so a `GREEN → YELLOW` transition is your early
|
|
6
|
+
warning.
|
|
7
|
+
|
|
8
|
+
`Whatsapp::Webhook::MessageTemplateQualityUpdate` · confidence: **moderate-high**
|
|
9
|
+
|
|
10
|
+
## Payload
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{ "field": "message_template_quality_update",
|
|
14
|
+
"value": {
|
|
15
|
+
"message_template_id": "123",
|
|
16
|
+
"message_template_name": "order_confirmation",
|
|
17
|
+
"message_template_language": "en_US",
|
|
18
|
+
"previous_quality_score": "GREEN",
|
|
19
|
+
"new_quality_score": "YELLOW"
|
|
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_quality_score` | `GREEN` \| `YELLOW` \| `RED` \| `UNKNOWN` |
|
|
31
|
+
| `new_quality_score` | Same set |
|
|
32
|
+
|
|
33
|
+
## Handling it
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
when "message_template_quality_update"
|
|
37
|
+
update = change.value
|
|
38
|
+
|
|
39
|
+
if update.new_quality_score == "RED"
|
|
40
|
+
Ops.alert("Template #{update.message_template_name} dropped to RED — pause the campaign")
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The same score is readable on demand via
|
|
45
|
+
[`Response::Node#quality_score`](../message_templates/responses.md#qualityscore).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# `message_template_status_update`
|
|
2
|
+
|
|
3
|
+
The review verdict on a template you submitted. This is **the** notification to handle
|
|
4
|
+
if you create templates from code — Meta reviews asynchronously and can take up to 24
|
|
5
|
+
hours, so this is how you learn the outcome without polling.
|
|
6
|
+
|
|
7
|
+
`Whatsapp::Webhook::MessageTemplateStatusUpdate` · confidence: **high**
|
|
8
|
+
|
|
9
|
+
## Payload
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "field": "message_template_status_update",
|
|
13
|
+
"value": {
|
|
14
|
+
"message_template_id": "123",
|
|
15
|
+
"message_template_name": "order_confirmation",
|
|
16
|
+
"message_template_language": "en_US",
|
|
17
|
+
"event": "REJECTED",
|
|
18
|
+
"reason": "INVALID_FORMAT"
|
|
19
|
+
} }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Accessors
|
|
23
|
+
|
|
24
|
+
| Accessor | Meaning |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `message_template_id` | The template's ID, as returned by `create` |
|
|
27
|
+
| `message_template_name` | Its name |
|
|
28
|
+
| `message_template_language` | Its locale — a template is one per `(name, language)` pair |
|
|
29
|
+
| `event` | `APPROVED` \| `REJECTED` \| `PAUSED` \| `PENDING_DELETION` \| … |
|
|
30
|
+
| `reason` | Why it was rejected: `NONE` `ABUSIVE_CONTENT` `INVALID_FORMAT` `PROMOTIONAL` `TAG_CONTENT_MISMATCH` `SCAM` |
|
|
31
|
+
|
|
32
|
+
## Handling it
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
when "message_template_status_update"
|
|
36
|
+
update = change.value
|
|
37
|
+
|
|
38
|
+
MessageTemplate
|
|
39
|
+
.find_by(meta_id: update.message_template_id)
|
|
40
|
+
&.update!(status: update.event, rejected_reason: update.reason)
|
|
41
|
+
|
|
42
|
+
if update.event == "REJECTED"
|
|
43
|
+
Rails.logger.warn(
|
|
44
|
+
"Template #{update.message_template_name} (#{update.message_template_language}) " \
|
|
45
|
+
"rejected: #{update.reason}"
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See [../message_templates/README.md](../message_templates/README.md) for the creation
|
|
51
|
+
side.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview
|