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
@@ -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>
@@ -0,0 +1,157 @@
1
+ # Business usernames
2
+
3
+ A business username is the searchable `@name` a WhatsApp user can find you by. It is the
4
+ business half of Meta's usernames rollout — the user half is what makes
5
+ [`recipient:`](../messages/README.md#addressing-to-recipient-or-both) necessary when
6
+ sending.
7
+
8
+ Two things it is **not**: it does not hide your business phone number in the app (unlike
9
+ a *user* username, which hides theirs), and it does not replace your display name. When
10
+ you have both, the display name shows first.
11
+
12
+ ```ruby
13
+ Whatsapp::BusinessPhoneNumber::Username::Get.call
14
+ Whatsapp::BusinessPhoneNumber::Username::Claim.call(username: "jaspersmarket")
15
+ Whatsapp::BusinessPhoneNumber::Username::Suggest.call
16
+ Whatsapp::BusinessPhoneNumber::Username::Delete.call
17
+ ```
18
+
19
+ | Method | Request | Returns |
20
+ | --- | --- | --- |
21
+ | `Get.call` | `GET /{phone_id}/username` | `Username::Details` |
22
+ | `Claim.call(username:, transfer_action:)` | `POST /{phone_id}/username` | `Username::Details` |
23
+ | `Delete.call` | `DELETE /{phone_id}/username` | `Response` |
24
+ | `Suggest.call` | `GET /{phone_id}/username_suggestions` | `Username::Suggestions` |
25
+
26
+ ## Reading the current username
27
+
28
+ ```ruby
29
+ details = Whatsapp::BusinessPhoneNumber::Username::Get.call
30
+
31
+ details.username # => "jaspersmarket", or nil if the number has none
32
+ details.status # => "approved" | "reserved"
33
+ details.approved? # => true once it is visible to WhatsApp users
34
+ details.reserved? # => held for you, but not visible yet
35
+ ```
36
+
37
+ `status` stays a raw String and is never validated on the way in, so a value Meta adds
38
+ later still round-trips. Compare against `Details::Statuses::ALL` (`%w[approved reserved]`).
39
+
40
+ ## The lifecycle
41
+
42
+ ```
43
+ Suggest ──▶ Claim ──▶ reserved ──▶ approved
44
+ (what Meta (ask for (Meta makes the feature
45
+ reserved one) generally available)
46
+ for you)
47
+ ```
48
+
49
+ `reserved` means the name is yours but not yet shown to users. It becomes `approved` on
50
+ its own — you do not claim it twice. Either poll `Get`, or subscribe to the
51
+ [`business_username_updates`](../webhooks/business_username_updates.md) webhook, which
52
+ is the asynchronous counterpart to everything on this page.
53
+
54
+ ## Claiming one
55
+
56
+ ```ruby
57
+ details = Whatsapp::BusinessPhoneNumber::Username::Claim.call(username: "jaspersmarket")
58
+ details.status # => "reserved"
59
+ ```
60
+
61
+ `Claim` answers with a status only — `details.username` is `nil`, because you already
62
+ hold the name you passed in.
63
+
64
+ Starting with names Meta reserved for you gives a higher chance of approval:
65
+
66
+ ```ruby
67
+ suggestions = Whatsapp::BusinessPhoneNumber::Username::Suggest.call
68
+
69
+ suggestions.to_a # => ["jaspersmarket", "jaspers_market"]
70
+ suggestions.first # => "jaspersmarket" — it's Enumerable
71
+
72
+ Whatsapp::BusinessPhoneNumber::Username::Claim.call(username: suggestions.first)
73
+ ```
74
+
75
+ ### Format rules
76
+
77
+ Checked locally, before any request — the edge is rate limited, so a name that was never
78
+ going to be accepted should not cost you an attempt:
79
+
80
+ | Rule | Rejected example |
81
+ | --- | --- |
82
+ | Only `a-z`, `0-9`, `.` and `_` | `jaspers-market`, `jasperś` |
83
+ | 3–35 characters | `ab`, `a` × 36 |
84
+ | At least one letter | `12345` |
85
+ | Must not start or end with `.` | `.jaspers`, `jaspers.` |
86
+ | No two consecutive `.` | `jaspers..market` |
87
+ | Must not start with `www` | `www.jaspers` |
88
+ | Must not end with a domain suffix | `jaspers.com`, `jaspers.html` |
89
+
90
+ Each failure raises `ActiveModel::ValidationError` naming the rule.
91
+
92
+ Three things worth knowing about how these are applied:
93
+
94
+ - **Mixed case is downcased, not rejected.** Meta compares usernames case-insensitively,
95
+ so `"JaspersMarket"` is sent as `"jaspersmarket"`. Refusing a name Meta would have
96
+ accepted is the failure mode this gem avoids.
97
+ - **`.` and `_` are significant.** `myid`, `my.id`, and `my_id` are three different names.
98
+ - **`www` is read literally**, so `wwwidgets` is rejected too. Meta writes the rule as a
99
+ bare prefix; a reversible local rejection beats a wasted request.
100
+
101
+ > The domain-suffix list is **`.com .org .net .int .edu .gov .mil .us .in .html`** —
102
+ > and Meta's own list ends in "and so on", so it cannot be complete. A suffix outside it
103
+ > reaches the API and comes back as error `100`.
104
+
105
+ ### Moving a username between your own numbers
106
+
107
+ If another phone number *in the same portfolio* holds the name, `Claim` fails with
108
+ `147005`. Resend with a forced transfer:
109
+
110
+ ```ruby
111
+ Whatsapp::BusinessPhoneNumber::Username::Claim.call(
112
+ username: "jaspersmarket",
113
+ transfer_action: :force_transfer
114
+ )
115
+ ```
116
+
117
+ > **This takes the name off the other number.** There is deliberately no automatic
118
+ > retry — a destructive step stays an explicit one.
119
+
120
+ ## Deleting one
121
+
122
+ ```ruby
123
+ Whatsapp::BusinessPhoneNumber::Username::Delete.call.success # => true
124
+ ```
125
+
126
+ Note this reports `success`, not a status: a delete arrives back through the
127
+ [webhook](../webhooks/business_username_updates.md) as `status: "deleted"`, which is the
128
+ only place that value ever appears — `Details::Statuses::ALL` deliberately omits it.
129
+
130
+ ## Errors
131
+
132
+ | Code | Meaning and remedy |
133
+ | --- | --- |
134
+ | `10` | The token's system user needs Full control, or Partial access for Phone numbers, on the WABA |
135
+ | `33` | Invalid phone number ID, deleted WABA, or the app lacks `whatsapp_business_management` |
136
+ | `100` | The username format is invalid — including a domain suffix outside the list above |
137
+ | `147001` | Already claimed, or failed Meta's internal checks. Try another |
138
+ | `147002` | The portfolio's [messaging limit](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits) is too low |
139
+ | `147003` | Link the number to the Facebook Page already using this name |
140
+ | `147004` | Link the number to the Instagram account already using this name |
141
+ | `147005` | Another of your numbers holds it — resend with `transfer_action: "force_transfer"` |
142
+ | `133010` | The number must be [registered](README.md) for API use first |
143
+
144
+ All arrive as `Whatsapp::BusinessPhoneNumber::Error` with Meta's message attached.
145
+
146
+ ## Targeting a different number
147
+
148
+ ```ruby
149
+ Whatsapp::BusinessPhoneNumber::Username::Get.call(client: Whatsapp::Client.new(phone_id: "OTHER_ID"))
150
+ ```
151
+
152
+ ---
153
+
154
+ **See also:** [`business_username_updates` webhook](../webhooks/business_username_updates.md) ·
155
+ [Sending to a BSUID](../messages/README.md#business-scoped-user-ids-bsuid)
156
+
157
+ **Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
@@ -0,0 +1,177 @@
1
+ # Configuration
2
+
3
+ `Whatsapp.configure` sets the global defaults every `Whatsapp::Client` is built from.
4
+ Call it once at boot.
5
+
6
+ ```ruby
7
+ Whatsapp.configure do |config|
8
+ config.api_key = ENV.fetch("WHATSAPP_API_KEY")
9
+ config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID")
10
+ end
11
+ ```
12
+
13
+ ## Options
14
+
15
+ | Option | Default | Needed for |
16
+ | --- | --- | --- |
17
+ | `api_key` | `nil` | Everything — a Meta system-user or app access token |
18
+ | `phone_id` | `nil` | [Messages](messages/README.md), [media](media/README.md), [phone-number onboarding](business_phone_number/README.md) |
19
+ | `waba_id` | `nil` | [Template management](message_templates/README.md), [subscribed apps](subscribed_app/README.md), [campaign schedules](schedule/README.md), [the business account](business_phone_number/account.md) |
20
+ | `verify_token` | `nil` | The [webhook GET handshake](webhooks/README.md#verification-the-get-handshake) |
21
+ | `app_secret` | `nil` | [Webhook signature verification](webhooks/README.md#signature-the-post-check) |
22
+ | `host` | `"https://graph.facebook.com"` | Overriding the API host |
23
+ | `version` | `"v24.0"` | Pinning a Graph API version |
24
+ | `media_host_allowlist` | `["lookaside.fbsbx.com", "mmg.whatsapp.net", "graph.facebook.com"]` | [Media download safety](media/README.md#the-token-never-leaves-the-allowlist) |
25
+
26
+ A full configuration:
27
+
28
+ ```ruby
29
+ Whatsapp.configure do |config|
30
+ config.api_key = ENV.fetch("WHATSAPP_API_KEY")
31
+ config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID")
32
+ config.waba_id = ENV.fetch("WHATSAPP_WABA_ID")
33
+ config.verify_token = ENV.fetch("WHATSAPP_VERIFY_TOKEN")
34
+ config.app_secret = ENV.fetch("WHATSAPP_APP_SECRET")
35
+ end
36
+ ```
37
+
38
+ In Rails, put that in `config/initializers/whatsapp.rb` and read from
39
+ `Rails.application.credentials` rather than `ENV`.
40
+
41
+ ## Which ID addresses what
42
+
43
+ The most common source of confusion. Two different Meta objects, two different
44
+ permissions:
45
+
46
+ ```
47
+ phone_id ──► Messages send_text!, send_template!, send_interactive!, ...
48
+ Media upload, get_url, download, delete
49
+ BusinessPhoneNumber RequestCode, VerifyCode, Register, Deregister
50
+ (permission: whatsapp_business_messaging)
51
+
52
+ waba_id ──► MessageTemplates create, list, find, update, delete, upsert
53
+ SubscribedApp List, Subscribe, Unsubscribe
54
+ Schedule List, Create
55
+ BusinessPhoneNumber::Account Get, Update
56
+ (permission: whatsapp_business_management)
57
+ ```
58
+
59
+ Meta's Schedules reference names no permission at all. `Schedule` is listed under
60
+ `whatsapp_business_management` because it addresses the WABA like its neighbours, but
61
+ that is an assumption, not something Meta documents.
62
+
63
+ `BusinessPhoneNumber` is the one module that appears on both sides: its onboarding
64
+ actions address a phone number, while its
65
+ [`Account`](business_phone_number/account.md) actions address the account that number
66
+ belongs to.
67
+
68
+ Calling a `waba_id` API without one raises before any request is made:
69
+
70
+ ```ruby
71
+ Whatsapp::MessageTemplates.new.list
72
+ # => Whatsapp::MessageTemplates::TemplateError: waba_id is required for template
73
+ # management; set it via Whatsapp.configure or Client.new(waba_id:)
74
+ ```
75
+
76
+ ## Secrets are redacted
77
+
78
+ `api_key`, `app_secret`, and `verify_token` never appear in `#inspect` output — not on
79
+ `Configuration`, not on `Client`, and therefore not on anything holding a client
80
+ (`Media`, `MessageTemplates`). This matters because error trackers routinely capture
81
+ local variables.
82
+
83
+ ```ruby
84
+ Whatsapp.configuration.inspect
85
+ # => "#<Whatsapp::Configuration host=\"https://graph.facebook.com\" version=\"v24.0\"
86
+ # api_key=[REDACTED] phone_id=\"106540352242922\" waba_id=nil
87
+ # verify_token=[REDACTED] app_secret=[REDACTED]>"
88
+
89
+ Whatsapp::Client.new.inspect
90
+ # => "#<Whatsapp::Client host=\"https://graph.facebook.com\" version=\"v24.0\"
91
+ # api_key=[REDACTED] phone_id=\"106540352242922\" waba_id=nil>"
92
+ ```
93
+
94
+ The same treatment applies to credentials in transit:
95
+ [`Register#inspect`](business_phone_number/README.md#registering) redacts the PIN and
96
+ `VerifyCode#inspect` redacts the OTP.
97
+
98
+ ## The client
99
+
100
+ Every API call goes through a `Whatsapp::Client`, which wraps a persistent HTTP
101
+ connection. All its constructor keywords default to `Whatsapp.configuration`:
102
+
103
+ ```ruby
104
+ Whatsapp::Client.new(
105
+ host: Whatsapp.configuration.host,
106
+ version: Whatsapp.configuration.version,
107
+ api_key: Whatsapp.configuration.api_key,
108
+ phone_id: Whatsapp.configuration.phone_id,
109
+ waba_id: Whatsapp.configuration.waba_id,
110
+ timeout: 30, # Client::DEFAULT_TIMEOUT, seconds
111
+ logger: nil
112
+ )
113
+ ```
114
+
115
+ Every entry point in the gem accepts an optional `client:`, which is how you address a
116
+ second phone number or business account without touching global state:
117
+
118
+ ```ruby
119
+ Whatsapp::Messages.send_text!(
120
+ to: "+15551234567", body: "Hi",
121
+ client: Whatsapp::Client.new(phone_id: "OTHER_PHONE_ID")
122
+ )
123
+
124
+ Whatsapp::MessageTemplates.new(client: Whatsapp::Client.new(waba_id: "OTHER_WABA_ID")).list
125
+ ```
126
+
127
+ ### Connection reuse
128
+
129
+ `Client#connection` memoizes `HTTP.persistent(host)`, so successive calls on the same
130
+ client reuse one TCP connection. Build a client once and hold it for a batch of sends
131
+ rather than constructing one per message:
132
+
133
+ ```ruby
134
+ client = Whatsapp::Client.new
135
+
136
+ recipients.each { |to| Whatsapp::Messages.send_text!(to:, body: "Hi", client:) }
137
+ ```
138
+
139
+ ### Timeouts
140
+
141
+ ```ruby
142
+ Whatsapp::Client.new(timeout: 10)
143
+ ```
144
+
145
+ Applies to the whole request. The default is 30 seconds.
146
+
147
+ ## Instrumentation
148
+
149
+ Pass a `Logger` to log every request and response:
150
+
151
+ ```ruby
152
+ client = Whatsapp::Client.new(logger: Logger.new($stdout))
153
+
154
+ Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi", client: client)
155
+ # I, [...] INFO -- : POST https://graph.facebook.com/v24.0/106540352242922/messages
156
+ # I, [...] INFO -- : 200 OK
157
+ ```
158
+
159
+ | Event | Logged |
160
+ | --- | --- |
161
+ | Request start | `<VERB> <url>` at `info` |
162
+ | Response | `<status> <reason>` at `info` |
163
+ | Error | `<name>: <message>` at `error` |
164
+
165
+ > **The query string is deliberately dropped.** Media and template endpoints put IDs
166
+ > in query parameters; logging them whole is how identifiers end up in log aggregators
167
+ > forever. Only the path is logged.
168
+
169
+ In Rails, pass `Rails.logger`:
170
+
171
+ ```ruby
172
+ Whatsapp::Client.new(logger: Rails.logger)
173
+ ```
174
+
175
+ ---
176
+
177
+ See also [errors.md](errors.md) for what the client raises.