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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -0
- data/README.md +322 -617
- data/docs/README.md +76 -0
- data/docs/business_phone_number/README.md +243 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/contact_book.md +90 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/business_phone_number/username.md +157 -0
- data/docs/configuration.md +177 -0
- data/docs/errors.md +219 -0
- data/docs/media/README.md +161 -0
- data/docs/message_templates/README.md +206 -0
- data/docs/message_templates/authentication.md +107 -0
- data/docs/message_templates/carousel.md +103 -0
- data/docs/message_templates/components.md +215 -0
- data/docs/message_templates/library.md +98 -0
- data/docs/message_templates/limited_time_offer.md +94 -0
- data/docs/message_templates/responses.md +173 -0
- data/docs/message_templates/standard.md +138 -0
- data/docs/messages/README.md +212 -0
- data/docs/messages/address.md +86 -0
- data/docs/messages/audio.md +43 -0
- data/docs/messages/contacts.md +129 -0
- data/docs/messages/document.md +49 -0
- data/docs/messages/image.md +59 -0
- data/docs/messages/interactive.md +342 -0
- data/docs/messages/location.md +51 -0
- data/docs/messages/location_request.md +59 -0
- data/docs/messages/mark_message_as_read.md +84 -0
- data/docs/messages/reaction.md +59 -0
- data/docs/messages/sticker.md +40 -0
- data/docs/messages/template.md +193 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/schedule/README.md +183 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +217 -0
- data/docs/webhooks/account_alerts.md +44 -0
- data/docs/webhooks/account_review_update.md +29 -0
- data/docs/webhooks/account_update.md +54 -0
- data/docs/webhooks/automatic_events.md +47 -0
- data/docs/webhooks/business_capability_update.md +40 -0
- data/docs/webhooks/business_username_updates.md +55 -0
- data/docs/webhooks/history.md +107 -0
- data/docs/webhooks/message_template_components_update.md +46 -0
- data/docs/webhooks/message_template_quality_update.md +49 -0
- data/docs/webhooks/message_template_status_update.md +55 -0
- data/docs/webhooks/messages.md +520 -0
- data/docs/webhooks/partner_solutions.md +35 -0
- data/docs/webhooks/payment_configuration_update.md +40 -0
- data/docs/webhooks/phone_number_name_update.md +44 -0
- data/docs/webhooks/phone_number_quality_update.md +40 -0
- data/docs/webhooks/security.md +43 -0
- data/docs/webhooks/smb_app_state_sync.md +91 -0
- data/docs/webhooks/smb_message_echoes.md +91 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_id_update.md +84 -0
- data/docs/webhooks/user_preferences.md +87 -0
- data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
- data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
- data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
- data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
- data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
- data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
- data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
- data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
- data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
- data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
- data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
- data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
- data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
- data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
- data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
- data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/messages/base.rb +47 -5
- data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
- data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
- data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/schedule/create.rb +101 -0
- data/lib/ruby/whatsapp/schedule/list.rb +50 -0
- data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
- data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
- data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
- data/lib/ruby/whatsapp/schedule/response.rb +18 -0
- data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
- data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
- data/lib/ruby/whatsapp/schedule.rb +15 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
- data/lib/ruby/whatsapp/webhook/change.rb +3 -1
- data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
- data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
- data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
- data/lib/ruby/whatsapp/webhook/history.rb +73 -34
- data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
- data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
- data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
- data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
- data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
- data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
- data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
- data/lib/ruby/whatsapp/webhook/message.rb +3 -0
- data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
- data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
- data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
- data/lib/ruby/whatsapp/webhook/status.rb +23 -2
- data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
- data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
- data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
- data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
- data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
- 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.
|