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
data/docs/README.md ADDED
@@ -0,0 +1,73 @@
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
+
21
+ ### Message kinds
22
+
23
+ [text](messages/text.md) ·
24
+ [image](messages/image.md) ·
25
+ [video](messages/video.md) ·
26
+ [audio](messages/audio.md) ·
27
+ [document](messages/document.md) ·
28
+ [sticker](messages/sticker.md) ·
29
+ [reaction](messages/reaction.md) ·
30
+ [location](messages/location.md) ·
31
+ [contacts](messages/contacts.md) ·
32
+ [address](messages/address.md) ·
33
+ [location request](messages/location_request.md) ·
34
+ [template](messages/template.md) ·
35
+ [interactive](messages/interactive.md) ·
36
+ [mark as read](messages/mark_message_as_read.md)
37
+
38
+ ### Template kinds
39
+
40
+ [standard](message_templates/standard.md) ·
41
+ [authentication](message_templates/authentication.md) ·
42
+ [carousel](message_templates/carousel.md) ·
43
+ [limited-time offer](message_templates/limited_time_offer.md) ·
44
+ [library](message_templates/library.md) ·
45
+ [components reference](message_templates/components.md) ·
46
+ [responses](message_templates/responses.md)
47
+
48
+ ## Inbound — hearing back
49
+
50
+ | Page | Covers |
51
+ | --- | --- |
52
+ | [webhooks/](webhooks/README.md) | Install, verification, signature, dispatch, all 19 notification fields |
53
+ | [webhooks/messages.md](webhooks/messages.md) | Inbound messages and delivery statuses — the one you'll actually use |
54
+
55
+ ## Account administration
56
+
57
+ | Page | Covers |
58
+ | --- | --- |
59
+ | [subscribed_app/](subscribed_app/README.md) | Turning webhook delivery on and off for a WABA |
60
+ | [business_phone_number/](business_phone_number/README.md) | Onboarding a phone number: request code → verify → register |
61
+ | [business_phone_number/account.md](business_phone_number/account.md) | Reading and updating the business account itself |
62
+ | [business_phone_number/profile.md](business_phone_number/profile.md) | Reading and updating the business profile a user sees |
63
+
64
+ ## Reading order
65
+
66
+ New to the Cloud API? Follow the setup path in order:
67
+
68
+ 1. [configuration.md](configuration.md) — get credentials in place
69
+ 2. [business_phone_number/](business_phone_number/README.md) — register the number
70
+ 3. [messages/text.md](messages/text.md) — send something
71
+ 4. [subscribed_app/](subscribed_app/README.md) — turn on webhook delivery
72
+ 5. [webhooks/](webhooks/README.md) — receive the reply
73
+ 6. [message_templates/](message_templates/README.md) — message outside the 24-hour window
@@ -0,0 +1,232 @@
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
+ > **Unrelated to [`SubscribedApp`](../subscribed_app/README.md).** That turns *webhook
31
+ > delivery* on and off for a whole WhatsApp Business Account. This is per phone number
32
+ > and has nothing to do with notifications.
33
+
34
+ ## The whole flow
35
+
36
+ ```ruby
37
+ Whatsapp.configure do |config|
38
+ config.api_key = ENV.fetch("WHATSAPP_API_KEY")
39
+ config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID")
40
+ end
41
+
42
+ Whatsapp::BusinessPhoneNumber::RequestCode.call(code_method: "SMS", language: "en_US")
43
+ Whatsapp::BusinessPhoneNumber::VerifyCode.call(code: "123456")
44
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834")
45
+ Whatsapp::BusinessPhoneNumber::Deregister.call
46
+ ```
47
+
48
+ | Method | Request | Returns |
49
+ | --- | --- | --- |
50
+ | `RequestCode.call(code_method:, language:)` | `POST /{phone_id}/request_code` | `Response` |
51
+ | `VerifyCode.call(code:)` | `POST /{phone_id}/verify_code` | `Response` (may carry `id`) |
52
+ | `Register.call(pin:, data_localization_region:)` | `POST /{phone_id}/register` | `Response` |
53
+ | `Deregister.call` | `POST /{phone_id}/deregister` | `Response` |
54
+ | [`Account::Get.call(fields:)`](account.md) | `GET /{waba_id}` | `Account::Details` |
55
+ | [`Account::Update.call(name:, timezone_id:)`](account.md) | `POST /{waba_id}` | `Response` |
56
+ | [`Profile::Get.call(fields:)`](profile.md) | `GET /{phone_id}/whatsapp_business_profile` | `Profile::Details` |
57
+ | [`Profile::Update.call(**fields)`](profile.md) | `POST /{phone_id}/whatsapp_business_profile` | `Response` |
58
+
59
+ Every action accepts an optional `client:` and raises
60
+ `Whatsapp::BusinessPhoneNumber::Error` if `phone_id` is missing or the API rejects the
61
+ request.
62
+
63
+ ## Requesting a verification code
64
+
65
+ Sends a one-time code to the number, by SMS or automated voice call.
66
+
67
+ ```ruby
68
+ Whatsapp::BusinessPhoneNumber::RequestCode.call(code_method: "SMS", language: "en_US").success
69
+ # => true
70
+ ```
71
+
72
+ | Field | Required | Rules |
73
+ | --- | --- | --- |
74
+ | `code_method` | yes | `"SMS"` or `"VOICE"` — either casing accepted, emitted uppercase |
75
+ | `language` | yes | The locale for the message, e.g. `"en_US"`. Presence only |
76
+
77
+ > `language` is deliberately **not** checked against `Utils::LanguageCodes`. That list
78
+ > is the set of locales Meta *approves templates* in; this field is closer to a
79
+ > delivery preference, and Meta documents it as a plain string with no enum.
80
+
81
+ ```ruby
82
+ Whatsapp::BusinessPhoneNumber::RequestCode.call(code_method: "CARRIER_PIGEON", language: "en_US")
83
+ # => ActiveModel::ValidationError: Code method is not included in the list
84
+ ```
85
+
86
+ Both fields are always sent — neither is compacted away:
87
+
88
+ ```ruby
89
+ Whatsapp::BusinessPhoneNumber::RequestCode.new(code_method: "sms", language: "en_US").serialize
90
+ # => { code_method: "SMS", language: "en_US" }
91
+ ```
92
+
93
+ ## Verifying the code
94
+
95
+ ```ruby
96
+ result = Whatsapp::BusinessPhoneNumber::VerifyCode.call(code: "123456")
97
+
98
+ result.success # => true
99
+ result.id # => "106540352242922" — the phone number ID, when Meta returns one
100
+ ```
101
+
102
+ `code` is checked for presence only — Meta documents no format, so a wrong-but-present
103
+ code is rejected server-side.
104
+
105
+ The OTP is redacted from `#inspect`:
106
+
107
+ ```ruby
108
+ Whatsapp::BusinessPhoneNumber::VerifyCode.new(code: "123456").inspect
109
+ # => "#<Whatsapp::BusinessPhoneNumber::VerifyCode code=[REDACTED]>"
110
+ ```
111
+
112
+ ## Registering
113
+
114
+ The step that actually activates the number on Cloud API.
115
+
116
+ ```ruby
117
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834").success
118
+ # => true
119
+ ```
120
+
121
+ | Field | Required | Rules |
122
+ | --- | --- | --- |
123
+ | `pin` | yes | Exactly **6 digits** — the existing two-step verification PIN, or the one to set |
124
+ | `data_localization_region` | no | One of 14 region codes |
125
+
126
+ ```ruby
127
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "1234")
128
+ # => ActiveModel::ValidationError: Pin must be exactly 6 digits
129
+ ```
130
+
131
+ ### Data localization
132
+
133
+ Passing a region enables local storage — message data at rest stays in that region:
134
+
135
+ ```ruby
136
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834", data_localization_region: "CH")
137
+ ```
138
+
139
+ | Region | Codes |
140
+ | --- | --- |
141
+ | APAC | `AU` `ID` `IN` `JP` `SG` `KR` |
142
+ | Europe | `DE` `CH` `GB` |
143
+ | LATAM | `BR` |
144
+ | MEA | `BH` `ZA` `AE` |
145
+ | NORAM | `CA` |
146
+
147
+ Omitted when not given:
148
+
149
+ ```ruby
150
+ Whatsapp::BusinessPhoneNumber::Register.new(pin: "212834").serialize
151
+ # => { messaging_product: "whatsapp", pin: "212834" }
152
+
153
+ Whatsapp::BusinessPhoneNumber::Register.new(pin: "212834", data_localization_region: "ch").serialize
154
+ # => { messaging_product: "whatsapp", pin: "212834", data_localization_region: "CH" }
155
+ ```
156
+
157
+ The PIN is redacted from `#inspect`, and no validation error message ever echoes it:
158
+
159
+ ```ruby
160
+ Whatsapp::BusinessPhoneNumber::Register.new(pin: "212834", data_localization_region: "CH").inspect
161
+ # => "#<Whatsapp::BusinessPhoneNumber::Register pin=[REDACTED] data_localization_region=\"CH\">"
162
+ ```
163
+
164
+ ## Deregistering
165
+
166
+ ```ruby
167
+ Whatsapp::BusinessPhoneNumber::Deregister.call.success # => true
168
+ ```
169
+
170
+ Deregistering makes the number unusable with Cloud API and disables local storage. It
171
+ does **not** delete the number or its message history. A number in use with both Cloud
172
+ API and the WhatsApp Business app cannot be deregistered.
173
+
174
+ `Deregister` takes no arguments and has nothing to validate — its payload is `{}`.
175
+
176
+ ## Targeting a different number
177
+
178
+ Inject a client rather than mutating global configuration:
179
+
180
+ ```ruby
181
+ Whatsapp::BusinessPhoneNumber::Register.call(
182
+ pin: "212834",
183
+ client: Whatsapp::Client.new(phone_id: "OTHER_PHONE_ID")
184
+ )
185
+ ```
186
+
187
+ ## Rate limits
188
+
189
+ > **`Register` / `Deregister`:** 10 requests per business number in a **72-hour moving
190
+ > window**. Exceeding it returns error `133016` and blocks the operation for the next
191
+ > 72 hours.
192
+ >
193
+ > **`RequestCode` / `VerifyCode`:** "standard Graph API rate limits", plus possible
194
+ > additional throttling — Meta publishes no number.
195
+
196
+ This is server-side state the gem cannot observe, so there is no client-side tracking.
197
+ It is also why every action validates locally first: a rejected attempt still counts
198
+ against the limit.
199
+
200
+ ## Errors
201
+
202
+ ```ruby
203
+ Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834")
204
+ # => Whatsapp::BusinessPhoneNumber::Error: phone_id is required for the register edge;
205
+ # set it via Whatsapp.configure or Client.new(phone_id:)
206
+
207
+ # non-2xx response
208
+ # => Whatsapp::BusinessPhoneNumber::Error: Failed to register business phone number:
209
+ # 400 Bad Request - {"error":{"message":"..."}}
210
+ ```
211
+
212
+ Local validation failures raise `ActiveModel::ValidationError` before any request. See
213
+ [../errors.md](../errors.md).
214
+
215
+ ## Not wrapped
216
+
217
+ - **Client-side rate-limit tracking** — server-side state the gem cannot observe.
218
+ - **Two-step verification PIN management.** `Register` *consumes* an existing PIN;
219
+ setting or changing one is a different endpoint.
220
+
221
+ ---
222
+
223
+ **See also:** [Reading and updating the business account](account.md) ·
224
+ [Reading and updating the business profile](profile.md)
225
+
226
+ ---
227
+
228
+ **Meta docs:**
229
+ [registration](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-phone-numbers/registration)
230
+ · [request code](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/phone-number-verification-request-code-api)
231
+ · [verify code](https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/verify-code-api)
232
+ · [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,220 @@
1
+ # Reading and Updating the Business Profile
2
+
3
+ A business phone number has a **business profile** — the public card a WhatsApp user sees
4
+ before they reply: the about line, the description, where the business is, how else to reach
5
+ it, and what industry it is in. The rest of this module drives whether a number *works*;
6
+ `Whatsapp::BusinessPhoneNumber::Profile` decides how it *looks*.
7
+
8
+ > **Same `phone_id` as the onboarding actions.** Meta exposes the profile as an edge on the
9
+ > phone number itself, so `Profile` addresses `phone_id` exactly like
10
+ > [`RequestCode`/`VerifyCode`/`Register`/`Deregister`](README.md) — unlike
11
+ > [`Account`](account.md), which is this module's one `waba_id`-scoped corner.
12
+
13
+ ```ruby
14
+ Whatsapp.configure do |config|
15
+ config.api_key = ENV.fetch("WHATSAPP_API_KEY")
16
+ config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID")
17
+ end
18
+
19
+ Whatsapp::BusinessPhoneNumber::Profile::Get.call.about # => "Open daily 9-5"
20
+ Whatsapp::BusinessPhoneNumber::Profile::Update.call(about: "Open 9-6") # => #<Response success=true>
21
+ ```
22
+
23
+ | Method | Request | Returns |
24
+ | --- | --- | --- |
25
+ | `Get.call(client:, fields:)` | `GET /{phone_id}/whatsapp_business_profile` | `Profile::Details` |
26
+ | `Update.call(client:, **fields)` | `POST /{phone_id}/whatsapp_business_profile` | `BusinessPhoneNumber::Response` |
27
+
28
+ Two independent actions, so two classes — the same shape as [`Account`](account.md) and
29
+ [subscribed apps](../subscribed_app/README.md), not the single-object CRUD of
30
+ [templates](../message_templates/README.md).
31
+
32
+ ## Reading the profile
33
+
34
+ ```ruby
35
+ profile = Whatsapp::BusinessPhoneNumber::Profile::Get.call
36
+
37
+ profile.about # => "Open daily 9-5"
38
+ profile.vertical # => "RETAIL"
39
+ profile.websites # => ["https://acme.test"]
40
+ ```
41
+
42
+ Ask for specific fields with `fields:` — an Array or a String, comma-joined for you:
43
+
44
+ ```ruby
45
+ Whatsapp::BusinessPhoneNumber::Profile::Get.call(fields: %w[about vertical websites])
46
+ Whatsapp::BusinessPhoneNumber::Profile::Get.call(fields: "about,vertical,websites")
47
+ ```
48
+
49
+ `Fields::ALL` requests everything Meta publishes, without hand-typing the list:
50
+
51
+ ```ruby
52
+ profile = Whatsapp::BusinessPhoneNumber::Profile::Get.call(
53
+ fields: Whatsapp::BusinessPhoneNumber::Profile::Get::Fields::ALL
54
+ )
55
+ ```
56
+
57
+ | Field | Description |
58
+ | --- | --- |
59
+ | `messaging_product` | The messaging service — always `"whatsapp"` in practice |
60
+ | `about` | The text shown in the profile's About section |
61
+ | `address` | The business's physical address |
62
+ | `description` | The business description |
63
+ | `email` | The business's contact email address |
64
+ | `profile_picture_url` | URL of the current profile picture |
65
+ | `websites` | The business's website URLs |
66
+ | `vertical` | One of `Profile::Verticals::ALL` |
67
+
68
+ > `fields` is **not** validated against `Fields::ALL`. Those constants are for
69
+ > convenience, not enforcement — a field Meta adds later works without a gem release.
70
+ > Only what Meta documents as a *client-side* rule is checked locally.
71
+
72
+ Meta wraps the read in a `data` array; `Profile::Details` unwraps it for you, so there is no
73
+ `.data.first` to reach through. Every field is optional in the response, so anything you did
74
+ not request is `nil` rather than an error.
75
+
76
+ > Read-side values are never validated or normalized on the way in. A `vertical` Meta adds
77
+ > later still round-trips untouched, so `profile.vertical` is always exactly what the API
78
+ > sent — compare it against `Verticals::ALL` rather than expecting `Details` to have
79
+ > rejected it. Same reasoning as [`Account::Details`](account.md#status-values)' statuses and
80
+ > [`MessageTemplates::Response::Node#components`](../message_templates/responses.md).
81
+
82
+ ## Updating the profile
83
+
84
+ ```ruby
85
+ Whatsapp::BusinessPhoneNumber::Profile::Update.call(
86
+ about: "Open daily 9-6", vertical: "RETAIL", websites: ["https://acme.test"]
87
+ ).success
88
+ # => true
89
+ ```
90
+
91
+ | Field | Required | Rules |
92
+ | --- | --- | --- |
93
+ | `about` | no | At most **139** characters |
94
+ | `address` | no | At most **256** characters |
95
+ | `description` | no | At most **512** characters |
96
+ | `email` | no | At most **128** characters. Format is *not* checked — see below |
97
+ | `vertical` | no | One of `Verticals::ALL` — either casing accepted, emitted uppercase |
98
+ | `websites` | no | At most **2** URLs |
99
+ | `profile_picture_handle` | no | Non-empty when given. From the Resumable Upload API |
100
+
101
+ Every field is optional, but **at least one must be present** — otherwise the request would
102
+ spend a call to change nothing:
103
+
104
+ ```ruby
105
+ Whatsapp::BusinessPhoneNumber::Profile::Update.call
106
+ # => ActiveModel::ValidationError: at least one of about, address, description, email,
107
+ # vertical, websites, profile_picture_handle must be provided
108
+ ```
109
+
110
+ The limits and the vertical enum are checked before anything is sent:
111
+
112
+ ```ruby
113
+ Whatsapp::BusinessPhoneNumber::Profile::Update.call(about: "a" * 140)
114
+ # => ActiveModel::ValidationError: About is too long (maximum is 139 characters)
115
+
116
+ Whatsapp::BusinessPhoneNumber::Profile::Update.call(vertical: "SPACESHIPS")
117
+ # => ActiveModel::ValidationError: Vertical is not included in the list
118
+
119
+ Whatsapp::BusinessPhoneNumber::Profile::Update.call(websites: %w[https://a.test https://b.test https://c.test])
120
+ # => ActiveModel::ValidationError: websites accepts at most 2 URLs
121
+ ```
122
+
123
+ > **`email`'s format is deliberately not validated.** Meta checks the address server-side,
124
+ > and a regex here would reject perfectly valid exotic addresses. This gem checks only
125
+ > length. Same standing decision as `override_callback_uri` in
126
+ > [subscribed apps](../subscribed_app/README.md) — no speculative hardening.
127
+
128
+ `messaging_product` is always sent for you. An omitted field is compacted out of the body
129
+ rather than sent as `null`, which Meta would read as an instruction to blank it:
130
+
131
+ ```ruby
132
+ Whatsapp::BusinessPhoneNumber::Profile::Update.new(about: "Open daily").serialize
133
+ # => { messaging_product: "whatsapp", about: "Open daily" }
134
+
135
+ Whatsapp::BusinessPhoneNumber::Profile::Update.new(vertical: "retail").serialize
136
+ # => { messaging_product: "whatsapp", vertical: "RETAIL" }
137
+ ```
138
+
139
+ ### Clearing the website list
140
+
141
+ An **empty array** is the one falsy-looking value that survives compaction, because that is
142
+ how Meta clears the list — and it counts as "changing something", so it satisfies the
143
+ at-least-one-field rule on its own:
144
+
145
+ ```ruby
146
+ Whatsapp::BusinessPhoneNumber::Profile::Update.new(websites: []).serialize
147
+ # => { messaging_product: "whatsapp", websites: [] }
148
+ ```
149
+
150
+ ### Industry verticals
151
+
152
+ ```ruby
153
+ Whatsapp::BusinessPhoneNumber::Profile::Verticals::ALL
154
+ ```
155
+
156
+ | Constant | Values |
157
+ | --- | --- |
158
+ | `Verticals::ALL` | `ALCOHOL` `APPAREL` `AUTO` `BEAUTY` `EDU` `ENTERTAIN` `EVENT_PLAN` `FINANCE` `GOVT` `GROCERY` `HEALTH` `HOTEL` `NONPROFIT` `ONLINE_GAMBLING` `OTC_DRUGS` `OTHER` `PHYSICAL_GAMBLING` `PROF_SERVICES` `RESTAURANT` `RETAIL` `TRAVEL` |
159
+
160
+ Either casing is accepted and normalized on the way out, so `"retail"`, `:retail`, and
161
+ `"RETAIL"` are the same request.
162
+
163
+ ### The profile picture
164
+
165
+ `profile_picture_handle` is forwarded as an opaque string. The handle itself comes from
166
+ Meta's **Resumable Upload API**, which this gem does not wrap:
167
+
168
+ ```ruby
169
+ Whatsapp::BusinessPhoneNumber::Profile::Update.call(profile_picture_handle: "4::aW1hZ2UvcG5n...")
170
+ ```
171
+
172
+ > **A media ID will not work here.** [`Media#upload`](../media/README.md) returns a media ID
173
+ > for *sending* messages — a different thing from an upload handle. Same standing exclusion
174
+ > as media handles for [template headers](../message_templates/README.md#not-wrapped).
175
+
176
+ `Update` returns the same `Whatsapp::BusinessPhoneNumber::Response` as the four onboarding
177
+ actions — the endpoint answers with a bare `{ "success": true }`, so there is no separate
178
+ response class for it.
179
+
180
+ ## Errors
181
+
182
+ ```ruby
183
+ Whatsapp::BusinessPhoneNumber::Profile::Get.call
184
+ # => Whatsapp::BusinessPhoneNumber::Error: phone_id is required for the
185
+ # whatsapp_business_profile edge; set it via Whatsapp.configure or Client.new(phone_id:)
186
+
187
+ # non-2xx response
188
+ # => Whatsapp::BusinessPhoneNumber::Error: Failed to update business profile:
189
+ # 400 Bad Request - {"error":{"message":"Invalid parameter: email must be a valid ..."}}
190
+ ```
191
+
192
+ Both actions raise `Whatsapp::BusinessPhoneNumber::Error` — the same class the onboarding
193
+ actions use, so one `rescue` covers the whole module. Local validation failures raise
194
+ `ActiveModel::ValidationError` before any request. See [../errors.md](../errors.md).
195
+
196
+ ## Targeting a different number
197
+
198
+ Inject a client rather than mutating global configuration:
199
+
200
+ ```ruby
201
+ Whatsapp::BusinessPhoneNumber::Profile::Get.call(
202
+ client: Whatsapp::Client.new(phone_id: "OTHER_PHONE_ID")
203
+ )
204
+ ```
205
+
206
+ ## Not wrapped
207
+
208
+ - **The Resumable Upload API** that mints a `profile_picture_handle` — the handle is accepted
209
+ and forwarded, but not produced.
210
+ - **Profile read caching.** Meta suggests caching profile reads for moderate periods;
211
+ invalidation is an application concern.
212
+
213
+ ---
214
+
215
+ **See also:** [Reading and updating the business account](account.md) ·
216
+ [Registering business phone numbers](README.md)
217
+
218
+ ---
219
+
220
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-phone-number/whatsapp-business-profile-api>