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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/README.md +322 -617
  4. data/docs/README.md +76 -0
  5. data/docs/business_phone_number/README.md +243 -0
  6. data/docs/business_phone_number/account.md +170 -0
  7. data/docs/business_phone_number/contact_book.md +90 -0
  8. data/docs/business_phone_number/profile.md +220 -0
  9. data/docs/business_phone_number/username.md +157 -0
  10. data/docs/configuration.md +177 -0
  11. data/docs/errors.md +219 -0
  12. data/docs/media/README.md +161 -0
  13. data/docs/message_templates/README.md +206 -0
  14. data/docs/message_templates/authentication.md +107 -0
  15. data/docs/message_templates/carousel.md +103 -0
  16. data/docs/message_templates/components.md +215 -0
  17. data/docs/message_templates/library.md +98 -0
  18. data/docs/message_templates/limited_time_offer.md +94 -0
  19. data/docs/message_templates/responses.md +173 -0
  20. data/docs/message_templates/standard.md +138 -0
  21. data/docs/messages/README.md +212 -0
  22. data/docs/messages/address.md +86 -0
  23. data/docs/messages/audio.md +43 -0
  24. data/docs/messages/contacts.md +129 -0
  25. data/docs/messages/document.md +49 -0
  26. data/docs/messages/image.md +59 -0
  27. data/docs/messages/interactive.md +342 -0
  28. data/docs/messages/location.md +51 -0
  29. data/docs/messages/location_request.md +59 -0
  30. data/docs/messages/mark_message_as_read.md +84 -0
  31. data/docs/messages/reaction.md +59 -0
  32. data/docs/messages/sticker.md +40 -0
  33. data/docs/messages/template.md +193 -0
  34. data/docs/messages/text.md +55 -0
  35. data/docs/messages/video.md +41 -0
  36. data/docs/schedule/README.md +183 -0
  37. data/docs/subscribed_app/README.md +139 -0
  38. data/docs/webhooks/README.md +217 -0
  39. data/docs/webhooks/account_alerts.md +44 -0
  40. data/docs/webhooks/account_review_update.md +29 -0
  41. data/docs/webhooks/account_update.md +54 -0
  42. data/docs/webhooks/automatic_events.md +47 -0
  43. data/docs/webhooks/business_capability_update.md +40 -0
  44. data/docs/webhooks/business_username_updates.md +55 -0
  45. data/docs/webhooks/history.md +107 -0
  46. data/docs/webhooks/message_template_components_update.md +46 -0
  47. data/docs/webhooks/message_template_quality_update.md +49 -0
  48. data/docs/webhooks/message_template_status_update.md +55 -0
  49. data/docs/webhooks/messages.md +520 -0
  50. data/docs/webhooks/partner_solutions.md +35 -0
  51. data/docs/webhooks/payment_configuration_update.md +40 -0
  52. data/docs/webhooks/phone_number_name_update.md +44 -0
  53. data/docs/webhooks/phone_number_quality_update.md +40 -0
  54. data/docs/webhooks/security.md +43 -0
  55. data/docs/webhooks/smb_app_state_sync.md +91 -0
  56. data/docs/webhooks/smb_message_echoes.md +91 -0
  57. data/docs/webhooks/template_category_update.md +52 -0
  58. data/docs/webhooks/user_id_update.md +84 -0
  59. data/docs/webhooks/user_preferences.md +87 -0
  60. data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
  61. data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
  62. data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
  63. data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
  64. data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
  65. data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
  66. data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
  67. data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
  68. data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
  69. data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
  70. data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
  71. data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
  72. data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
  73. data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
  74. data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
  75. data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
  76. data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
  77. data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
  78. data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
  79. data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
  80. data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
  81. data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
  82. data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
  83. data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
  84. data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
  85. data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
  86. data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
  87. data/lib/ruby/whatsapp/message_templates.rb +2 -6
  88. data/lib/ruby/whatsapp/messages/base.rb +47 -5
  89. data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
  90. data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
  91. data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
  92. data/lib/ruby/whatsapp/path_building.rb +33 -0
  93. data/lib/ruby/whatsapp/schedule/create.rb +101 -0
  94. data/lib/ruby/whatsapp/schedule/list.rb +50 -0
  95. data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
  96. data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
  97. data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
  98. data/lib/ruby/whatsapp/schedule/response.rb +18 -0
  99. data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
  100. data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
  101. data/lib/ruby/whatsapp/schedule.rb +15 -0
  102. data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
  103. data/lib/ruby/whatsapp/version.rb +1 -1
  104. data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
  105. data/lib/ruby/whatsapp/webhook/change.rb +3 -1
  106. data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
  107. data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
  108. data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
  109. data/lib/ruby/whatsapp/webhook/history.rb +73 -34
  110. data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
  111. data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
  112. data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
  113. data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
  114. data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
  115. data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
  116. data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
  117. data/lib/ruby/whatsapp/webhook/message.rb +3 -0
  118. data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
  119. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
  120. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
  121. data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
  122. data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
  123. data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
  124. data/lib/ruby/whatsapp/webhook/status.rb +23 -2
  125. data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
  126. data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
  127. data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
  128. data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
  129. data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
  130. metadata +100 -1
data/docs/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # ruby-whatsapp documentation
2
+
3
+ Full reference for [ruby-whatsapp](https://github.com/saleszera/ruby-whatsapp). The
4
+ [project README](../README.md) is the short version.
5
+
6
+ ## Start here
7
+
8
+ | Page | Covers |
9
+ | --- | --- |
10
+ | [configuration.md](configuration.md) | Credentials, the client, connection reuse, instrumentation, which ID addresses what |
11
+ | [errors.md](errors.md) | The full exception hierarchy, validation vs API failures, retry strategy |
12
+
13
+ ## Outbound — talking to customers
14
+
15
+ | Page | Covers |
16
+ | --- | --- |
17
+ | [messages/](messages/README.md) | Sending every message kind, the shared envelope, responses |
18
+ | [media/](media/README.md) | Upload, download, delete, and the token-safety allowlist |
19
+ | [message_templates/](message_templates/README.md) | Creating and managing the templates you send |
20
+ | [schedule/](schedule/README.md) | Scheduling an approved template to go out to an audience later |
21
+
22
+ ### Message kinds
23
+
24
+ [text](messages/text.md) ·
25
+ [image](messages/image.md) ·
26
+ [video](messages/video.md) ·
27
+ [audio](messages/audio.md) ·
28
+ [document](messages/document.md) ·
29
+ [sticker](messages/sticker.md) ·
30
+ [reaction](messages/reaction.md) ·
31
+ [location](messages/location.md) ·
32
+ [contacts](messages/contacts.md) ·
33
+ [address](messages/address.md) ·
34
+ [location request](messages/location_request.md) ·
35
+ [template](messages/template.md) ·
36
+ [interactive](messages/interactive.md) ·
37
+ [mark as read](messages/mark_message_as_read.md)
38
+
39
+ ### Template kinds
40
+
41
+ [standard](message_templates/standard.md) ·
42
+ [authentication](message_templates/authentication.md) ·
43
+ [carousel](message_templates/carousel.md) ·
44
+ [limited-time offer](message_templates/limited_time_offer.md) ·
45
+ [library](message_templates/library.md) ·
46
+ [components reference](message_templates/components.md) ·
47
+ [responses](message_templates/responses.md)
48
+
49
+ ## Inbound — hearing back
50
+
51
+ | Page | Covers |
52
+ | --- | --- |
53
+ | [webhooks/](webhooks/README.md) | Install, verification, signature, dispatch, all 21 notification fields |
54
+ | [webhooks/messages.md](webhooks/messages.md) | Inbound messages and delivery statuses — the one you'll actually use |
55
+
56
+ ## Account administration
57
+
58
+ | Page | Covers |
59
+ | --- | --- |
60
+ | [subscribed_app/](subscribed_app/README.md) | Turning webhook delivery on and off for a WABA |
61
+ | [business_phone_number/](business_phone_number/README.md) | Onboarding a phone number: request code → verify → register |
62
+ | [business_phone_number/account.md](business_phone_number/account.md) | Reading and updating the business account itself |
63
+ | [business_phone_number/profile.md](business_phone_number/profile.md) | Reading and updating the business profile a user sees |
64
+ | [business_phone_number/username.md](business_phone_number/username.md) | Claiming and managing the searchable business username |
65
+ | [business_phone_number/contact_book.md](business_phone_number/contact_book.md) | Removing a stored user contact from Meta's contact book |
66
+
67
+ ## Reading order
68
+
69
+ New to the Cloud API? Follow the setup path in order:
70
+
71
+ 1. [configuration.md](configuration.md) — get credentials in place
72
+ 2. [business_phone_number/](business_phone_number/README.md) — register the number
73
+ 3. [messages/text.md](messages/text.md) — send something
74
+ 4. [subscribed_app/](subscribed_app/README.md) — turn on webhook delivery
75
+ 5. [webhooks/](webhooks/README.md) — receive the reply
76
+ 6. [message_templates/](message_templates/README.md) — message outside the 24-hour window
@@ -0,0 +1,243 @@
1
+ # Registering Business Phone Numbers
2
+
3
+ A business phone number is unusable with Cloud API until it is **registered**.
4
+ Registration is the prerequisite that makes sending, media, and templates work for that
5
+ number at all — not an optional extra.
6
+
7
+ `Whatsapp::BusinessPhoneNumber` wraps all four endpoints in that path:
8
+
9
+ ```
10
+ RequestCode -> VerifyCode -> Register (onboarding)
11
+ (send OTP) (confirm it) (activate on Cloud API)
12
+
13
+ Deregister (the reverse switch)
14
+ ```
15
+
16
+ Addresses your **phone number** (`phone_id`, not `waba_id`), like
17
+ [messages](../messages/README.md) and [media](../media/README.md), and needs both the
18
+ `whatsapp_business_messaging` and `whatsapp_business_management` permissions.
19
+
20
+ > **Looking for the account itself?** [`Account::Get` / `Account::Update`](account.md)
21
+ > read and update the WhatsApp Business Account a number belongs to — its name,
22
+ > timezone, review and verification status. Those are the one `waba_id`-scoped corner
23
+ > of this module.
24
+
25
+ > **Looking for the profile a user sees?** [`Profile::Get` / `Profile::Update`](profile.md)
26
+ > read and write the about line, description, address, email, websites, industry vertical,
27
+ > and picture. Those address your phone number (`phone_id`), just like the four actions
28
+ > below.
29
+
30
+ > **Looking for the username?** [`Username::Get` / `Claim` / `Delete` / `Suggest`](username.md)
31
+ > read and set the searchable `@name` a WhatsApp user can find the number by, and
32
+ > [`ContactBook::Delete`](contact_book.md) forgets a stored user contact. Both address
33
+ > `phone_id`, like the four actions below.
34
+
35
+ > **Unrelated to [`SubscribedApp`](../subscribed_app/README.md).** That turns *webhook
36
+ > delivery* on and off for a whole WhatsApp Business Account. This is per phone number
37
+ > and has nothing to do with notifications.
38
+
39
+ ## The whole flow
40
+
41
+ ```ruby
42
+ Whatsapp.configure do |config|
43
+ config.api_key = ENV.fetch("WHATSAPP_API_KEY")
44
+ config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID")
45
+ end
46
+
47
+ Whatsapp::BusinessPhoneNumber::RequestCode.call(code_method: "SMS", language: "en_US")
48
+ Whatsapp::BusinessPhoneNumber::VerifyCode.call(code: "123456")
49
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834")
50
+ Whatsapp::BusinessPhoneNumber::Deregister.call
51
+ ```
52
+
53
+ | Method | Request | Returns |
54
+ | --- | --- | --- |
55
+ | `RequestCode.call(code_method:, language:)` | `POST /{phone_id}/request_code` | `Response` |
56
+ | `VerifyCode.call(code:)` | `POST /{phone_id}/verify_code` | `Response` (may carry `id`) |
57
+ | `Register.call(pin:, data_localization_region:)` | `POST /{phone_id}/register` | `Response` |
58
+ | `Deregister.call` | `POST /{phone_id}/deregister` | `Response` |
59
+ | [`Account::Get.call(fields:)`](account.md) | `GET /{waba_id}` | `Account::Details` |
60
+ | [`Account::Update.call(name:, timezone_id:)`](account.md) | `POST /{waba_id}` | `Response` |
61
+ | [`Profile::Get.call(fields:)`](profile.md) | `GET /{phone_id}/whatsapp_business_profile` | `Profile::Details` |
62
+ | [`Profile::Update.call(**fields)`](profile.md) | `POST /{phone_id}/whatsapp_business_profile` | `Response` |
63
+ | [`Username::Get.call`](username.md) | `GET /{phone_id}/username` | `Username::Details` |
64
+ | [`Username::Claim.call(username:, transfer_action:)`](username.md) | `POST /{phone_id}/username` | `Username::Details` |
65
+ | [`Username::Delete.call`](username.md) | `DELETE /{phone_id}/username` | `Response` |
66
+ | [`Username::Suggest.call`](username.md) | `GET /{phone_id}/username_suggestions` | `Username::Suggestions` |
67
+ | [`ContactBook::Delete.call(bsuid:)`](contact_book.md) | `DELETE /{phone_id}/contact_book` | `ContactBook::Deletion` |
68
+
69
+ Every action accepts an optional `client:` and raises
70
+ `Whatsapp::BusinessPhoneNumber::Error` if `phone_id` is missing or the API rejects the
71
+ request.
72
+
73
+ ## Requesting a verification code
74
+
75
+ Sends a one-time code to the number, by SMS or automated voice call.
76
+
77
+ ```ruby
78
+ Whatsapp::BusinessPhoneNumber::RequestCode.call(code_method: "SMS", language: "en_US").success
79
+ # => true
80
+ ```
81
+
82
+ | Field | Required | Rules |
83
+ | --- | --- | --- |
84
+ | `code_method` | yes | `"SMS"` or `"VOICE"` — either casing accepted, emitted uppercase |
85
+ | `language` | yes | The locale for the message, e.g. `"en_US"`. Presence only |
86
+
87
+ > `language` is deliberately **not** checked against `Utils::LanguageCodes`. That list
88
+ > is the set of locales Meta *approves templates* in; this field is closer to a
89
+ > delivery preference, and Meta documents it as a plain string with no enum.
90
+
91
+ ```ruby
92
+ Whatsapp::BusinessPhoneNumber::RequestCode.call(code_method: "CARRIER_PIGEON", language: "en_US")
93
+ # => ActiveModel::ValidationError: Code method is not included in the list
94
+ ```
95
+
96
+ Both fields are always sent — neither is compacted away:
97
+
98
+ ```ruby
99
+ Whatsapp::BusinessPhoneNumber::RequestCode.new(code_method: "sms", language: "en_US").serialize
100
+ # => { code_method: "SMS", language: "en_US" }
101
+ ```
102
+
103
+ ## Verifying the code
104
+
105
+ ```ruby
106
+ result = Whatsapp::BusinessPhoneNumber::VerifyCode.call(code: "123456")
107
+
108
+ result.success # => true
109
+ result.id # => "106540352242922" — the phone number ID, when Meta returns one
110
+ ```
111
+
112
+ `code` is checked for presence only — Meta documents no format, so a wrong-but-present
113
+ code is rejected server-side.
114
+
115
+ The OTP is redacted from `#inspect`:
116
+
117
+ ```ruby
118
+ Whatsapp::BusinessPhoneNumber::VerifyCode.new(code: "123456").inspect
119
+ # => "#<Whatsapp::BusinessPhoneNumber::VerifyCode code=[REDACTED]>"
120
+ ```
121
+
122
+ ## Registering
123
+
124
+ The step that actually activates the number on Cloud API.
125
+
126
+ ```ruby
127
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834").success
128
+ # => true
129
+ ```
130
+
131
+ | Field | Required | Rules |
132
+ | --- | --- | --- |
133
+ | `pin` | yes | Exactly **6 digits** — the existing two-step verification PIN, or the one to set |
134
+ | `data_localization_region` | no | One of 14 region codes |
135
+
136
+ ```ruby
137
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "1234")
138
+ # => ActiveModel::ValidationError: Pin must be exactly 6 digits
139
+ ```
140
+
141
+ ### Data localization
142
+
143
+ Passing a region enables local storage — message data at rest stays in that region:
144
+
145
+ ```ruby
146
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834", data_localization_region: "CH")
147
+ ```
148
+
149
+ | Region | Codes |
150
+ | --- | --- |
151
+ | APAC | `AU` `ID` `IN` `JP` `SG` `KR` |
152
+ | Europe | `DE` `CH` `GB` |
153
+ | LATAM | `BR` |
154
+ | MEA | `BH` `ZA` `AE` |
155
+ | NORAM | `CA` |
156
+
157
+ Omitted when not given:
158
+
159
+ ```ruby
160
+ Whatsapp::BusinessPhoneNumber::Register.new(pin: "212834").serialize
161
+ # => { messaging_product: "whatsapp", pin: "212834" }
162
+
163
+ Whatsapp::BusinessPhoneNumber::Register.new(pin: "212834", data_localization_region: "ch").serialize
164
+ # => { messaging_product: "whatsapp", pin: "212834", data_localization_region: "CH" }
165
+ ```
166
+
167
+ The PIN is redacted from `#inspect`, and no validation error message ever echoes it:
168
+
169
+ ```ruby
170
+ Whatsapp::BusinessPhoneNumber::Register.new(pin: "212834", data_localization_region: "CH").inspect
171
+ # => "#<Whatsapp::BusinessPhoneNumber::Register pin=[REDACTED] data_localization_region=\"CH\">"
172
+ ```
173
+
174
+ ## Deregistering
175
+
176
+ ```ruby
177
+ Whatsapp::BusinessPhoneNumber::Deregister.call.success # => true
178
+ ```
179
+
180
+ Deregistering makes the number unusable with Cloud API and disables local storage. It
181
+ does **not** delete the number or its message history. A number in use with both Cloud
182
+ API and the WhatsApp Business app cannot be deregistered.
183
+
184
+ `Deregister` takes no arguments and has nothing to validate — its payload is `{}`.
185
+
186
+ ## Targeting a different number
187
+
188
+ Inject a client rather than mutating global configuration:
189
+
190
+ ```ruby
191
+ Whatsapp::BusinessPhoneNumber::Register.call(
192
+ pin: "212834",
193
+ client: Whatsapp::Client.new(phone_id: "OTHER_PHONE_ID")
194
+ )
195
+ ```
196
+
197
+ ## Rate limits
198
+
199
+ > **`Register` / `Deregister`:** 10 requests per business number in a **72-hour moving
200
+ > window**. Exceeding it returns error `133016` and blocks the operation for the next
201
+ > 72 hours.
202
+ >
203
+ > **`RequestCode` / `VerifyCode`:** "standard Graph API rate limits", plus possible
204
+ > additional throttling — Meta publishes no number.
205
+
206
+ This is server-side state the gem cannot observe, so there is no client-side tracking.
207
+ It is also why every action validates locally first: a rejected attempt still counts
208
+ against the limit.
209
+
210
+ ## Errors
211
+
212
+ ```ruby
213
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834")
214
+ # => Whatsapp::BusinessPhoneNumber::Error: phone_id is required for the register edge;
215
+ # set it via Whatsapp.configure or Client.new(phone_id:)
216
+
217
+ # non-2xx response
218
+ # => Whatsapp::BusinessPhoneNumber::Error: Failed to register business phone number:
219
+ # 400 Bad Request - {"error":{"message":"..."}}
220
+ ```
221
+
222
+ Local validation failures raise `ActiveModel::ValidationError` before any request. See
223
+ [../errors.md](../errors.md).
224
+
225
+ ## Not wrapped
226
+
227
+ - **Client-side rate-limit tracking** — server-side state the gem cannot observe.
228
+ - **Two-step verification PIN management.** `Register` *consumes* an existing PIN;
229
+ setting or changing one is a different endpoint.
230
+
231
+ ---
232
+
233
+ **See also:** [Reading and updating the business account](account.md) ·
234
+ [Reading and updating the business profile](profile.md) ·
235
+ [Business usernames](username.md) · [Contact book](contact_book.md)
236
+
237
+ ---
238
+
239
+ **Meta docs:**
240
+ [registration](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration)
241
+ · [request code](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/phone-number-verification-request-code-api)
242
+ · [verify code](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/verify-code-api)
243
+ · [deregister](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/phone-number-deregister-api)
@@ -0,0 +1,170 @@
1
+ # Reading and Updating the Business Account
2
+
3
+ Every business phone number belongs to a **WhatsApp Business Account** (WABA). The rest
4
+ of this module drives a *number's* onboarding; `Whatsapp::BusinessPhoneNumber::Account`
5
+ reads and writes the *account* that number sits inside — whether Meta has approved it,
6
+ whether the business behind it is verified, who owns it, and what it is called.
7
+
8
+ > **The one `waba_id` corner of this module.**
9
+ > [`RequestCode`/`VerifyCode`/`Register`/`Deregister`](README.md) address your phone
10
+ > number (`phone_id`). `Account` addresses the account (`waba_id`), like
11
+ > [template management](../message_templates/README.md) and
12
+ > [subscribed apps](../subscribed_app/README.md), and needs the
13
+ > `whatsapp_business_management` permission.
14
+
15
+ ```ruby
16
+ Whatsapp.configure do |config|
17
+ config.api_key = ENV.fetch("WHATSAPP_API_KEY")
18
+ config.waba_id = ENV.fetch("WHATSAPP_WABA_ID")
19
+ end
20
+
21
+ Whatsapp::BusinessPhoneNumber::Account::Get.call.name # => "Acme Corp"
22
+ Whatsapp::BusinessPhoneNumber::Account::Update.call(name: "Acme") # => #<Response success=true>
23
+ ```
24
+
25
+ | Method | Request | Returns |
26
+ | --- | --- | --- |
27
+ | `Get.call(client:, fields:)` | `GET /{waba_id}` | `Account::Details` |
28
+ | `Update.call(client:, name:, timezone_id:)` | `POST /{waba_id}` | `BusinessPhoneNumber::Response` |
29
+
30
+ Two independent actions, so two classes — the same shape as
31
+ [subscribed apps](../subscribed_app/README.md), not the single-object CRUD of
32
+ [templates](../message_templates/README.md).
33
+
34
+ ## Reading account details
35
+
36
+ ```ruby
37
+ details = Whatsapp::BusinessPhoneNumber::Account::Get.call
38
+
39
+ details.id # => "102290129340398"
40
+ details.name # => "Acme Corp"
41
+ ```
42
+
43
+ By default Meta returns only `id` and `name`. Ask for more with `fields:` — an Array or
44
+ a String, comma-joined for you:
45
+
46
+ ```ruby
47
+ Whatsapp::BusinessPhoneNumber::Account::Get.call(fields: %w[id name country])
48
+ Whatsapp::BusinessPhoneNumber::Account::Get.call(fields: "id,name,country")
49
+ ```
50
+
51
+ `Fields::ALL` requests everything Meta publishes, without hand-typing the list:
52
+
53
+ ```ruby
54
+ details = Whatsapp::BusinessPhoneNumber::Account::Get.call(
55
+ fields: Whatsapp::BusinessPhoneNumber::Account::Get::Fields::ALL
56
+ )
57
+ ```
58
+
59
+ | Field | Description |
60
+ | --- | --- |
61
+ | `id` | The account's unique identifier |
62
+ | `name` | Human-readable account name |
63
+ | `timezone_id` | Timezone identifier |
64
+ | `message_template_namespace` | Namespace the account's templates live in |
65
+ | `account_review_status` | One of `Details::ReviewStatuses::ALL` |
66
+ | `business_verification_status` | One of `Details::VerificationStatuses::ALL` |
67
+ | `country` | Country code |
68
+ | `ownership_type` | One of `Details::OwnershipTypes::ALL` |
69
+ | `primary_business_location` | Primary business location |
70
+
71
+ > `fields` is **not** validated against `Fields::ALL`. Those constants are for
72
+ > convenience, not enforcement — a field Meta adds later works without a gem release.
73
+ > Only what Meta documents as a *client-side* rule is checked locally.
74
+
75
+ ### Status values
76
+
77
+ Statuses come back as plain strings, with frozen constants to compare against and three
78
+ predicates for the checks you actually make:
79
+
80
+ ```ruby
81
+ details.approved? # => true — account_review_status == "APPROVED"
82
+ details.verified? # => true — business_verification_status == "VERIFIED"
83
+ details.self_owned? # => true — ownership_type == "SELF"
84
+ ```
85
+
86
+ | Constant | Values |
87
+ | --- | --- |
88
+ | `Details::ReviewStatuses::ALL` | `APPROVED` `DEFERRED` `PENDING` `REJECTED` |
89
+ | `Details::VerificationStatuses::ALL` | `EXPIRED` `FAILED` `INELIGIBLE` `NOT_VERIFIED` `PENDING` `PENDING_NEED_MORE_INFO` `PENDING_SUBMISSION` `REJECTED` `REVOKED` `VERIFIED` |
90
+ | `Details::OwnershipTypes::ALL` | `CLIENT_OWNED` `ON_BEHALF_OF` `SELF` |
91
+
92
+ > Read-side values are never validated on the way in. A status Meta adds later still
93
+ > round-trips untouched, so `details.account_review_status` is always exactly what the
94
+ > API sent — same reasoning as
95
+ > [`MessageTemplates::Response::Node#components`](../message_templates/responses.md).
96
+
97
+ Every field is optional in the response, so anything you did not request is `nil`
98
+ rather than an error.
99
+
100
+ ## Updating the account
101
+
102
+ ```ruby
103
+ Whatsapp::BusinessPhoneNumber::Account::Update.call(name: "Acme Corporation").success
104
+ # => true
105
+ ```
106
+
107
+ | Field | Required | Rules |
108
+ | --- | --- | --- |
109
+ | `name` | no | Non-empty when given |
110
+ | `timezone_id` | no | Non-empty when given. Meta publishes no enum — presence only |
111
+
112
+ Both fields are optional, but **at least one must be present** — otherwise the request
113
+ would spend a call to change nothing:
114
+
115
+ ```ruby
116
+ Whatsapp::BusinessPhoneNumber::Account::Update.call
117
+ # => ActiveModel::ValidationError: at least one of name or timezone_id must be provided
118
+
119
+ Whatsapp::BusinessPhoneNumber::Account::Update.call(name: "")
120
+ # => ActiveModel::ValidationError: Name can't be blank
121
+ ```
122
+
123
+ An omitted field is compacted out of the body rather than sent as `null`, which Meta
124
+ would read as an instruction to blank it:
125
+
126
+ ```ruby
127
+ Whatsapp::BusinessPhoneNumber::Account::Update.new(name: "Acme Corp").serialize
128
+ # => { name: "Acme Corp" }
129
+
130
+ Whatsapp::BusinessPhoneNumber::Account::Update.new(name: "Acme Corp", timezone_id: "1").serialize
131
+ # => { name: "Acme Corp", timezone_id: "1" }
132
+ ```
133
+
134
+ `Update` returns the same `Whatsapp::BusinessPhoneNumber::Response` as the four
135
+ onboarding actions — the endpoint answers with a bare `{ "success": true }`, so there is
136
+ no separate response class for it.
137
+
138
+ ## Errors
139
+
140
+ ```ruby
141
+ Whatsapp::BusinessPhoneNumber::Account::Get.call
142
+ # => Whatsapp::BusinessPhoneNumber::Error: waba_id is required for business account
143
+ # details; set it via Whatsapp.configure or Client.new(waba_id:)
144
+
145
+ # non-2xx response
146
+ # => Whatsapp::BusinessPhoneNumber::Error: Failed to get business account details:
147
+ # 404 Not Found - {"error":{"message":"WhatsApp Business Account not found",...}}
148
+ ```
149
+
150
+ Both actions raise `Whatsapp::BusinessPhoneNumber::Error` — the same class the
151
+ onboarding actions use, so one `rescue` covers the whole module. Local validation
152
+ failures raise `ActiveModel::ValidationError` before any request. See
153
+ [../errors.md](../errors.md).
154
+
155
+ ## Targeting a different account
156
+
157
+ ```ruby
158
+ Whatsapp::BusinessPhoneNumber::Account::Get.call(
159
+ client: Whatsapp::Client.new(waba_id: "OTHER_WABA_ID")
160
+ )
161
+ ```
162
+
163
+ ---
164
+
165
+ **See also:** [Reading and updating the business profile](profile.md) ·
166
+ [Registering business phone numbers](README.md)
167
+
168
+ ---
169
+
170
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/whatsapp-business-account-api>
@@ -0,0 +1,90 @@
1
+ # Contact book
2
+
3
+ Meta keeps a **contact book** for your business portfolio: the phone numbers and BSUIDs
4
+ of users you have interacted with. Its purpose is thread continuity — once a user adopts
5
+ a username, Meta will keep putting their phone number in your webhooks if they are in
6
+ your contact book, instead of leaving you with only their
7
+ [BSUID](../webhooks/messages.md#business-scoped-user-ids-bsuid).
8
+
9
+ The store itself is hosted by Meta and needs no integration work; entries appear on their
10
+ own as you exchange messages or calls, and when a user taps a `REQUEST_CONTACT_INFO`
11
+ button (which arrives as a [`contacts` message](../webhooks/messages.md#contacts) with
12
+ `origin == "contact_request"`). This page covers the one operation Meta exposes on it:
13
+ removing an entry.
14
+
15
+ | Method | Request | Returns |
16
+ | --- | --- | --- |
17
+ | `ContactBook::Delete.call(bsuid:)` | `DELETE /{phone_id}/contact_book` | `ContactBook::Deletion` |
18
+
19
+ ## Deleting an entry
20
+
21
+ ```ruby
22
+ result = Whatsapp::BusinessPhoneNumber::ContactBook::Delete.call(bsuid: "US.13491208655302741918")
23
+
24
+ result.success # => true — the request was processed
25
+ result.deleted # => true — an entry existed and is gone
26
+ ```
27
+
28
+ The two booleans are independent, which is why this returns its own class rather than the
29
+ shared `Response`:
30
+
31
+ | `success` | `deleted` | Meaning |
32
+ | --- | --- | --- |
33
+ | `true` | `true` | An entry existed and was removed |
34
+ | `true` | `false` | The request was fine; there was nothing stored for that BSUID |
35
+ | `false` | — | The request was not processed |
36
+
37
+ So `deleted == false` is **not** a failure. Check `success` for that, or rescue
38
+ `Whatsapp::BusinessPhoneNumber::Error`.
39
+
40
+ ## What it affects
41
+
42
+ > **The edge is on one phone number; the effect is portfolio-wide.** Contact books are
43
+ > scoped to the business portfolio, so deleting an entry removes that user's phone number
44
+ > from webhooks for **every** business phone number in the portfolio — not just the one
45
+ > you addressed.
46
+
47
+ And the consequence to expect: after a deletion, `wa_id`/`from` may start being omitted
48
+ from your webhooks for that user, leaving their BSUID as the only identifier. That is the
49
+ feature working, not a bug — but code that keys off `wa_id` will notice.
50
+
51
+ The phone number can come back, without a new deletion, if the user messages or calls you
52
+ again (or you them) within the 30-day window Meta caches, since that recreates the entry.
53
+
54
+ ## Rules
55
+
56
+ - **Standard BSUIDs only.** Parent BSUIDs are not supported. Since this release hands you
57
+ `user_id` and `parent_user_id` side by side on half a dozen webhook classes, the mix-up
58
+ is easy, so it is caught locally:
59
+
60
+ ```ruby
61
+ Whatsapp::BusinessPhoneNumber::ContactBook::Delete.call(bsuid: "US.ENT.11815799212886844830")
62
+ # => ActiveModel::ValidationError: Bsuid looks like a parent BSUID;
63
+ # the contact book accepts standard BSUIDs only
64
+ ```
65
+
66
+ Detected by segment count rather than by matching `ENT`, which would bet that `ENT` is
67
+ the only marker Meta will ever use.
68
+
69
+ - **Same portfolio.** The BSUID must belong to the same portfolio as the phone number.
70
+ That is account state the gem cannot see, so it is left to the API — a mismatch comes
71
+ back as `Whatsapp::BusinessPhoneNumber::Error`.
72
+
73
+ ## Disabling the feature entirely
74
+
75
+ The contact book can be turned off in **Meta Business Suite → Business settings →
76
+ Business info**. Disabling it stops all storage and deletes everything already stored;
77
+ re-enabling starts fresh, with no way to restore the old contents. There is no API for
78
+ this, so it is not wrapped.
79
+
80
+ ## Not wrapped
81
+
82
+ Meta documents only the delete on this edge. A future read or write belongs in this same
83
+ namespace.
84
+
85
+ ---
86
+
87
+ **See also:** [Business usernames](username.md) ·
88
+ [BSUIDs in webhooks](../webhooks/messages.md#business-scoped-user-ids-bsuid)
89
+
90
+ **Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids