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,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