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,84 @@
1
+ # Mark Message As Read
2
+
3
+ Closes the read-receipt loop — this is what turns on the blue "seen" checkmarks on the
4
+ customer's side. Marking one message read also marks **every earlier message in that
5
+ conversation** as read.
6
+
7
+ ```ruby
8
+ Whatsapp::Messages.mark_message_as_read!(
9
+ message_id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJBMTE4RTUA"
10
+ )
11
+ ```
12
+
13
+ ## Fields
14
+
15
+ | Field | Required | Rules |
16
+ | --- | --- | --- |
17
+ | `message_id` | yes | The WAMID of an inbound message |
18
+ | `client` | no | Defaults to a new `Whatsapp::Client` |
19
+
20
+ `message_id` is checked for presence only — there is no WAMID format check, so an
21
+ invalid ID surfaces server-side as error `131009`.
22
+
23
+ > **30-day window.** Meta requires the call within 30 days of receiving the message.
24
+ > Past that, the request fails.
25
+
26
+ ## Why it isn't a `send_<kind>!` method
27
+
28
+ Every other kind shares the `to` / `recipient_type` / `type` envelope from
29
+ `Messages::Base`. This endpoint's payload is flat — no recipient, no type — so
30
+ `MarkMessageAsRead` doesn't inherit `Base` at all and isn't registered in
31
+ `Messages::KINDS`. Rather than carve an exception into the loop that generates
32
+ `send_<kind>!` methods, it gets its own hand-written class method.
33
+
34
+ ## Serialized payload
35
+
36
+ ```ruby
37
+ Whatsapp::Messages::MarkMessageAsRead.new(message_id: "wamid.HBg...").serialize
38
+ # => { messaging_product: "whatsapp", status: "read", message_id: "wamid.HBg..." }
39
+ ```
40
+
41
+ ## The response
42
+
43
+ This endpoint replies `{"success": true}` rather than the usual
44
+ `{messaging_product, contacts, messages}`, so the shared `Messages::Response` carries
45
+ it on `#success`:
46
+
47
+ ```ruby
48
+ result = Whatsapp::Messages.mark_message_as_read!(message_id: "wamid.HBg...")
49
+
50
+ result.success # => true
51
+ result.messages # => [] — nothing to report for this endpoint
52
+ result.contacts # => []
53
+ ```
54
+
55
+ ## Errors
56
+
57
+ ```ruby
58
+ Whatsapp::Messages::MarkMessageAsRead.new(message_id: "")
59
+ # => ActiveModel::ValidationError: Message can't be blank
60
+ ```
61
+
62
+ A non-2xx response raises `Whatsapp::RequestError: Failed to mark message as read: ...`.
63
+
64
+ ## Typical use
65
+
66
+ Call it from your webhook handler as soon as an inbound message is deserialized:
67
+
68
+ ```ruby
69
+ notification.entry.each do |entry|
70
+ entry.changes.each do |change|
71
+ next unless change.field == "messages"
72
+
73
+ change.value.messages.each do |message|
74
+ Whatsapp::Messages.mark_message_as_read!(message_id: message.id)
75
+ end
76
+ end
77
+ end
78
+ ```
79
+
80
+ See [../webhooks/messages.md](../webhooks/messages.md) for the inbound side.
81
+
82
+ ---
83
+
84
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/mark-message-as-read>
@@ -0,0 +1,59 @@
1
+ # Reaction Messages
2
+
3
+ Acknowledges a previous message with an emoji, mirroring WhatsApp's native
4
+ tap-and-hold reaction, without cluttering the thread with a new text message.
5
+
6
+ ```ruby
7
+ Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: "👍")
8
+ ```
9
+
10
+ ## Fields
11
+
12
+ | Field | Required | Rules |
13
+ | --- | --- | --- |
14
+ | `to` | yes | The recipient's phone number |
15
+ | `message_id` | yes | The WAMID of the message being reacted to |
16
+ | `emoji` | yes | A single emoji, or `""` to remove |
17
+
18
+ `emoji` is checked against Ruby's `\p{Emoji}` property, so a plain word is rejected
19
+ locally rather than by Meta. `nil` is invalid; the empty string is not.
20
+
21
+ ## Removing a reaction
22
+
23
+ Sending an empty `emoji` removes a reaction you previously sent to that message:
24
+
25
+ ```ruby
26
+ Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: "")
27
+ ```
28
+
29
+ ## Serialized payload
30
+
31
+ ```ruby
32
+ Whatsapp::Messages::Reaction.new(
33
+ to: "+15551234567", message_id: "wamid.HBg...", emoji: "\u{1F44D}"
34
+ ).serialize
35
+ # => {
36
+ # messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "reaction",
37
+ # reaction: { message_id: "wamid.HBg...", emoji: "👍" }
38
+ # }
39
+ ```
40
+
41
+ The `reaction` hash is not compacted — both keys always ship, which is what makes the
42
+ empty-string removal work.
43
+
44
+ ## Validation errors
45
+
46
+ ```ruby
47
+ Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: "nope")
48
+ # => ActiveModel::ValidationError: Emoji must be a valid emoji character
49
+
50
+ Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: nil)
51
+ # => ActiveModel::ValidationError: Emoji can't be nil
52
+ ```
53
+
54
+ `message_id` is checked for presence only — an existent-looking but wrong WAMID
55
+ surfaces server-side as error `131009`.
56
+
57
+ ---
58
+
59
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/reaction-messages>
@@ -0,0 +1,40 @@
1
+ # Sticker Messages
2
+
3
+ Sends a WhatsApp sticker asset (a WebP image, animated or static).
4
+
5
+ ```ruby
6
+ Whatsapp::Messages.send_sticker!(to: "+15551234567", id: "STICKER_MEDIA_ID")
7
+ ```
8
+
9
+ ## Fields
10
+
11
+ | Field | Required | Rules |
12
+ | --- | --- | --- |
13
+ | `to` | yes | The recipient's phone number |
14
+ | `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
15
+ | `link` | one of | A publicly reachable HTTPS URL |
16
+
17
+ **Either `id` or `link` is required** — prefer `id`. Stickers are almost always
18
+ reused across many conversations, so uploading once and holding the ID avoids
19
+ re-fetching the same asset on every send.
20
+
21
+ ## Serialized payload
22
+
23
+ ```ruby
24
+ Whatsapp::Messages::Sticker.new(to: "+15551234567", id: "STICKER_MEDIA_ID").serialize
25
+ # => {
26
+ # messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "sticker",
27
+ # sticker: { id: "STICKER_MEDIA_ID" }
28
+ # }
29
+ ```
30
+
31
+ ## Validation errors
32
+
33
+ ```ruby
34
+ Whatsapp::Messages.send_sticker!(to: "+15551234567")
35
+ # => ActiveModel::ValidationError: Either id or link must be present
36
+ ```
37
+
38
+ ---
39
+
40
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/sticker-messages>
@@ -0,0 +1,166 @@
1
+ # Template Messages
2
+
3
+ The **only** way to message a user outside the 24-hour customer-service window.
4
+ Marketing, utility, and authentication templates must be created and approved by Meta
5
+ before they can be sent.
6
+
7
+ > **Two different APIs.** This page covers *sending* an already-approved template
8
+ > (`POST /{PHONE_ID}/messages`). Creating, editing, and deleting the templates
9
+ > themselves is a separate API addressing `waba_id` — see
10
+ > [../message_templates/](../message_templates/README.md). The component schemas are
11
+ > **not** interchangeable: management uses `text` + `example` with uppercase
12
+ > `HEADER`/`BODY`/`FOOTER`, sending uses `parameters` with lowercase singular types.
13
+
14
+ ```ruby
15
+ Whatsapp::Messages.send_template!(
16
+ to: "+15551234567",
17
+ name: "order_confirmation",
18
+ language: { code: "en_US" }
19
+ )
20
+ ```
21
+
22
+ ## Fields
23
+
24
+ | Field | Required | Rules |
25
+ | --- | --- | --- |
26
+ | `to` | yes | The recipient's phone number |
27
+ | `name` | yes | The approved template's name |
28
+ | `language` | yes | `{ code: "en_US" }` — validated against the supported list |
29
+ | `components` | no | Array of components supplying placeholder values. Defaults to `[]` |
30
+
31
+ `language.code` is checked against `Whatsapp::Utils::LanguageCodes` (71 codes), so a
32
+ typo raises locally instead of coming back as a Meta error:
33
+
34
+ ```ruby
35
+ Whatsapp::Utils::LanguageCodes.valid?("en_US") # => true
36
+ Whatsapp::Utils::LanguageCodes.all.size # => 71
37
+ ```
38
+
39
+ ## Components
40
+
41
+ A component supplies the *values* for the placeholders the template defines. It has a
42
+ `type`, and for buttons a `sub_type` and `index`.
43
+
44
+ | `type` | Purpose |
45
+ | --- | --- |
46
+ | `"header"` | Values for the header's placeholder |
47
+ | `"body"` | Values for the body's placeholders |
48
+ | `"button"` | Values for a dynamic button — also needs `sub_type` and `index` |
49
+
50
+ Each component holds `parameters`, and each parameter has a `type` selecting exactly
51
+ one value key:
52
+
53
+ | Parameter `type` | Value key | Shape |
54
+ | --- | --- | --- |
55
+ | `"text"` | `text` | String |
56
+ | `"currency"` | `currency` | `{ fallback_value:, code:, amount_1000: }` |
57
+ | `"date_time"` | `date_time` | `{ fallback_value: }` |
58
+ | `"image"` | `image` | `{ link: }` or `{ id: }` |
59
+ | `"document"` | `document` | `{ link: }` or `{ id: }` |
60
+ | `"video"` | `video` | `{ link: }` or `{ id: }` |
61
+ | `"location"` | `location` | `{ latitude:, longitude:, name:, address: }` |
62
+ | `"payload"` | `payload` | String — a quick-reply button's payload |
63
+
64
+ > Only `type` is validated. There is no cross-field check that the matching value key
65
+ > is populated, so `{ type: "text" }` with no `text` serializes to a bare
66
+ > `{ type: "text" }` and is rejected by Meta rather than locally.
67
+
68
+ ## Examples
69
+
70
+ **Body placeholders:**
71
+
72
+ ```ruby
73
+ Whatsapp::Messages.send_template!(
74
+ to: "+15551234567",
75
+ name: "order_confirmation",
76
+ language: { code: "en_US" },
77
+ components: [
78
+ { type: "body", parameters: [{ type: "text", text: "Jane" },
79
+ { type: "text", text: "860198-230332" }] },
80
+ ]
81
+ )
82
+ ```
83
+
84
+ **Media header plus body:**
85
+
86
+ ```ruby
87
+ Whatsapp::Messages.send_template!(
88
+ to: "+15551234567",
89
+ name: "shipping_update",
90
+ language: { code: "en_US" },
91
+ components: [
92
+ { type: "header", parameters: [{ type: "image", image: { link: "https://example.com/box.jpg" } }] },
93
+ { type: "body", parameters: [{ type: "text", text: "Jane" }] },
94
+ ]
95
+ )
96
+ ```
97
+
98
+ **A dynamic URL button** — `sub_type` and `index` identify which button in the
99
+ template's button row is being filled:
100
+
101
+ ```ruby
102
+ Whatsapp::Messages.send_template!(
103
+ to: "+15551234567",
104
+ name: "order_confirmation",
105
+ language: { code: "en_US" },
106
+ components: [
107
+ { type: "body", parameters: [{ type: "text", text: "Jane" }] },
108
+ { type: "button", sub_type: "url", index: 0,
109
+ parameters: [{ type: "text", text: "1234" }] },
110
+ ]
111
+ )
112
+ ```
113
+
114
+ **A currency and a date:**
115
+
116
+ ```ruby
117
+ components: [
118
+ { type: "body", parameters: [
119
+ { type: "currency", currency: { fallback_value: "$100.99", code: "USD", amount_1000: 100_990 } },
120
+ { type: "date_time", date_time: { fallback_value: "February 25, 1977" } },
121
+ ] },
122
+ ]
123
+ ```
124
+
125
+ ## Serialized payload
126
+
127
+ ```ruby
128
+ Whatsapp::Messages::Template.new(
129
+ to: "+15551234567",
130
+ name: "order_confirmation",
131
+ language: { code: "en_US" },
132
+ components: [
133
+ { type: "body", parameters: [{ type: "text", text: "Jane" }, { type: "text", text: "#1234" }] },
134
+ ]
135
+ ).serialize
136
+ # => {
137
+ # messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "template",
138
+ # template: {
139
+ # name: "order_confirmation",
140
+ # language: { code: "en_US" },
141
+ # components: [{ type: "body", parameters: [{ type: "text", text: "Jane" },
142
+ # { type: "text", text: "#1234" }] }]
143
+ # }
144
+ # }
145
+ ```
146
+
147
+ With no components, the `components` key is dropped entirely rather than sent empty.
148
+
149
+ ## Validation errors
150
+
151
+ ```ruby
152
+ Whatsapp::Messages.send_template!(to: "+15551234567", name: "x", language: { code: "klingon" })
153
+ # => ActiveModel::ValidationError: Code is not a valid WhatsApp language code.
154
+ # Supported codes: af, sq, ar, az, bn, bg, ca, zh_CN, ...
155
+
156
+ Whatsapp::Messages.send_template!(to: "+15551234567", name: "", language: { code: "en_US" })
157
+ # => ActiveModel::ValidationError: Name can't be blank
158
+ ```
159
+
160
+ `name` is checked for presence only — an unapproved or misspelled template name comes
161
+ back from Meta, not from the validator.
162
+
163
+ ---
164
+
165
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/template-messages>
166
+ · [supported languages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages)
@@ -0,0 +1,55 @@
1
+ # Text Messages
2
+
3
+ The default way to carry a conversational reply or notice. Most other kinds exist
4
+ because they need to express something plain text can't — media, buttons, structured
5
+ data.
6
+
7
+ ```ruby
8
+ Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello from ruby-whatsapp!")
9
+ ```
10
+
11
+ ## Fields
12
+
13
+ | Field | Required | Rules |
14
+ | --- | --- | --- |
15
+ | `to` | yes | The recipient's phone number |
16
+ | `body` | yes | Max **4096** characters |
17
+ | `preview_url` | no | `true` or `false`. **Defaults to `true`** |
18
+
19
+ `preview_url` controls whether WhatsApp renders a link preview card for the first URL
20
+ in the body. Turn it off for transactional messages where a preview would be noise:
21
+
22
+ ```ruby
23
+ Whatsapp::Messages.send_text!(
24
+ to: "+15551234567",
25
+ body: "Track your order: https://example.com/orders/1234",
26
+ preview_url: false
27
+ )
28
+ ```
29
+
30
+ ## Serialized payload
31
+
32
+ ```ruby
33
+ Whatsapp::Messages::Text.new(to: "+15551234567", body: "Hello from ruby-whatsapp!").serialize
34
+ # => {
35
+ # messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "text",
36
+ # text: { body: "Hello from ruby-whatsapp!", preview_url: true }
37
+ # }
38
+ ```
39
+
40
+ The `text` hash is deliberately **not** compacted, so `preview_url` is always present
41
+ on the wire even when it holds its default.
42
+
43
+ ## Validation errors
44
+
45
+ ```ruby
46
+ Whatsapp::Messages.send_text!(to: "+15551234567", body: "")
47
+ # => ActiveModel::ValidationError: Body can't be blank
48
+
49
+ Whatsapp::Messages.send_text!(to: "+15551234567", body: "x" * 4097)
50
+ # => ActiveModel::ValidationError: Body is too long (maximum is 4096 characters)
51
+ ```
52
+
53
+ ---
54
+
55
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/text-messages>
@@ -0,0 +1,41 @@
1
+ # Video Messages
2
+
3
+ Shares a video — an instructional clip, a product demo, an unboxing.
4
+
5
+ ```ruby
6
+ Whatsapp::Messages.send_video!(to: "+15551234567", id: "1234567890", caption: "Demo video")
7
+ ```
8
+
9
+ ## Fields
10
+
11
+ | Field | Required | Rules |
12
+ | --- | --- | --- |
13
+ | `to` | yes | The recipient's phone number |
14
+ | `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
15
+ | `link` | one of | A publicly reachable HTTPS URL |
16
+ | `caption` | no | Max **1024** characters |
17
+
18
+ **Either `id` or `link` is required.** Videos are the kind where uploading once and
19
+ reusing the `id` pays off most — a `link` makes Meta re-download the whole file on
20
+ every single send.
21
+
22
+ ## Serialized payload
23
+
24
+ ```ruby
25
+ Whatsapp::Messages::Video.new(to: "+15551234567", id: "1234567890", caption: "Demo video").serialize
26
+ # => {
27
+ # messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "video",
28
+ # video: { id: "1234567890", caption: "Demo video" }
29
+ # }
30
+ ```
31
+
32
+ ## Validation errors
33
+
34
+ ```ruby
35
+ Whatsapp::Messages.send_video!(to: "+15551234567")
36
+ # => ActiveModel::ValidationError: Either id or link must be present
37
+ ```
38
+
39
+ ---
40
+
41
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/video-messages>
@@ -0,0 +1,139 @@
1
+ # Managing Subscribed Apps
2
+
3
+ Before your app receives any [webhook](../webhooks/README.md) notifications for a
4
+ WhatsApp Business Account, it has to be **subscribed** to it.
5
+ `Whatsapp::SubscribedApp` wraps the `subscribed_apps` edge — the switch that turns
6
+ webhook delivery on or off.
7
+
8
+ > **Two halves of one thing.** This module makes Meta start sending;
9
+ > [`Whatsapp::Webhook`](../webhooks/README.md) deserializes what arrives. Neither is
10
+ > useful without the other.
11
+
12
+ Addresses your **WhatsApp Business Account** (`waba_id`, not `phone_id`) and needs the
13
+ `whatsapp_business_management` permission — same as
14
+ [template management](../message_templates/README.md).
15
+
16
+ ```ruby
17
+ Whatsapp.configure do |config|
18
+ config.api_key = ENV.fetch("WHATSAPP_API_KEY")
19
+ config.waba_id = ENV.fetch("WHATSAPP_WABA_ID")
20
+ end
21
+
22
+ Whatsapp::SubscribedApp::Subscribe.call # start webhook delivery
23
+ Whatsapp::SubscribedApp::List.call.map(&:name) # => ["My App"]
24
+ Whatsapp::SubscribedApp::Unsubscribe.call # stop it
25
+ ```
26
+
27
+ | Method | Request | Returns |
28
+ | --- | --- | --- |
29
+ | `List.call(client:, fields:)` | `GET /{waba_id}/subscribed_apps` | `Response::Collection` |
30
+ | `Subscribe.call(client:, override_callback_uri:, verify_token:)` | `POST /{waba_id}/subscribed_apps` | `Response::Subscription` |
31
+ | `Unsubscribe.call(client:)` | `DELETE /{waba_id}/subscribed_apps` | `Response::Unsubscription` |
32
+
33
+ Three independent actions, so three classes rather than one class with three methods —
34
+ unlike [templates](../message_templates/README.md), subscribing and listing share no
35
+ identity or validation rules that would justify combining them.
36
+
37
+ ## Subscribing
38
+
39
+ ```ruby
40
+ result = Whatsapp::SubscribedApp::Subscribe.call
41
+
42
+ result.success # => true
43
+ result.map(&:name) # => ["My App"] — every app now subscribed, Meta's own echo
44
+ ```
45
+
46
+ `Response::Subscription` is `Enumerable` over the same `Response::App` objects `List`
47
+ returns, so you can check the result without a second request.
48
+
49
+ ### Per-WABA callback overrides
50
+
51
+ Tech Providers routing several WABAs' notifications to different callback URLs pass an
52
+ override instead of relying on the single callback URL configured on the app itself:
53
+
54
+ ```ruby
55
+ Whatsapp::SubscribedApp::Subscribe.call(
56
+ override_callback_uri: "https://example.com/webhooks/acme_corp",
57
+ verify_token: "a-per-account-secret"
58
+ )
59
+ ```
60
+
61
+ Both are optional and compacted out of the request body when absent. The
62
+ `verify_token` you pass here is the one Meta will send in the
63
+ [GET handshake](../webhooks/README.md#verification-the-get-handshake) for that WABA —
64
+ pass the matching value to `Verification.call(verify_token:)`.
65
+
66
+ > No client-side format check is applied to `override_callback_uri`. Meta enforces it
67
+ > server-side, and this gem validates only what Meta documents as a client-side rule.
68
+
69
+ ## Listing
70
+
71
+ ```ruby
72
+ apps = Whatsapp::SubscribedApp::List.call
73
+
74
+ apps.map(&:name) # Collection is Enumerable
75
+ apps.first.id # => "123456789"
76
+ apps.first.link # => "https://www.facebook.com/games/?app_id=..."
77
+ apps.first.override_callback_uri # => nil, or the per-WABA override
78
+ ```
79
+
80
+ Restrict what comes back with `fields:` — an Array or a String, comma-joined for you:
81
+
82
+ ```ruby
83
+ Whatsapp::SubscribedApp::List.call(fields: %w[id name])
84
+ Whatsapp::SubscribedApp::List.call(fields: "id,name")
85
+ ```
86
+
87
+ `Response::App` flattens Meta's `whatsapp_business_api_data` wrapper, so `id`, `name`,
88
+ and `link` sit directly on the object rather than a nested hash.
89
+
90
+ > No pagination — Meta documents none for this edge, and an account typically has very
91
+ > few subscribed apps.
92
+
93
+ ## Unsubscribing
94
+
95
+ ```ruby
96
+ Whatsapp::SubscribedApp::Unsubscribe.call.success # => true
97
+ ```
98
+
99
+ Stops **all** webhook deliveries for this WABA immediately.
100
+
101
+ ## Response shapes
102
+
103
+ Three actions, three genuinely different shapes — hence three classes:
104
+
105
+ | Class | Holds | From |
106
+ | --- | --- | --- |
107
+ | `Response::Collection` | `data: [App]`, `Enumerable` | `List` |
108
+ | `Response::Subscription` | `success`, `data: [App]`, `Enumerable` | `Subscribe` |
109
+ | `Response::Unsubscription` | `success` | `Unsubscribe` |
110
+
111
+ `Response::App` — `id`, `name`, `link`, `override_callback_uri` — is composed by both
112
+ `Collection` and `Subscription` rather than duplicated.
113
+
114
+ `success` is a strict `response["success"] == true`, so it is never a truthy string
115
+ you have to second-guess.
116
+
117
+ ## Errors
118
+
119
+ ```ruby
120
+ Whatsapp::SubscribedApp::Subscribe.call
121
+ # => Whatsapp::SubscribedApp::Error: waba_id is required for subscribed apps;
122
+ # set it via Whatsapp.configure or Client.new(waba_id:)
123
+
124
+ # non-2xx response
125
+ # => Whatsapp::SubscribedApp::Error: Failed to subscribe app: 400 Bad Request - {...}
126
+ ```
127
+
128
+ Every action raises `Whatsapp::SubscribedApp::Error` — never the generic
129
+ `Whatsapp::RequestError`. See [../errors.md](../errors.md).
130
+
131
+ ## Targeting a different account
132
+
133
+ ```ruby
134
+ Whatsapp::SubscribedApp::List.call(client: Whatsapp::Client.new(waba_id: "OTHER_WABA_ID"))
135
+ ```
136
+
137
+ ---
138
+
139
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/subscribed-apps-api>