ruby-whatsapp 0.4.2 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +158 -0
- data/README.md +70 -7
- data/docs/README.md +4 -1
- data/docs/business_phone_number/README.md +12 -1
- data/docs/business_phone_number/contact_book.md +90 -0
- data/docs/business_phone_number/username.md +157 -0
- data/docs/configuration.md +6 -1
- data/docs/errors.md +12 -1
- data/docs/message_templates/README.md +1 -1
- data/docs/message_templates/components.md +37 -1
- data/docs/messages/README.md +86 -9
- data/docs/messages/address.md +1 -1
- data/docs/messages/audio.md +1 -1
- data/docs/messages/contacts.md +1 -1
- data/docs/messages/document.md +1 -1
- data/docs/messages/image.md +1 -1
- data/docs/messages/interactive.md +84 -4
- data/docs/messages/location.md +1 -1
- data/docs/messages/location_request.md +1 -1
- data/docs/messages/reaction.md +1 -1
- data/docs/messages/sticker.md +1 -1
- data/docs/messages/template.md +28 -1
- data/docs/messages/text.md +1 -1
- data/docs/messages/video.md +1 -1
- data/docs/schedule/README.md +183 -0
- data/docs/webhooks/README.md +20 -12
- data/docs/webhooks/business_username_updates.md +55 -0
- data/docs/webhooks/history.md +78 -15
- data/docs/webhooks/messages.md +141 -6
- data/docs/webhooks/smb_app_state_sync.md +50 -12
- data/docs/webhooks/smb_message_echoes.md +58 -21
- data/docs/webhooks/user_id_update.md +84 -0
- data/docs/webhooks/user_preferences.md +55 -16
- 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/transport.rb +6 -5
- 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 +9 -0
- 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/messages/base.rb +47 -5
- data/lib/ruby/whatsapp/messages/interactive/list_buttons/section.rb +11 -6
- data/lib/ruby/whatsapp/messages/interactive/list_buttons.rb +12 -0
- 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/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/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 +38 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c33c221aee102d5b25468617f808a4f91c2e54a5659922576cd542dff802458
|
|
4
|
+
data.tar.gz: 10d24cb87f52e82ba182b4360bd25c6e2ae1fe9b8bb78d4d6141c17b2dc6c91f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd104f12c9ec44b4b07e5912f18e98ae4646a4eb1f51869b27dcc978b76ee06d385e81a6d41ce449b96460f08e0d2ddfb56b4d57ce410d3493f45e243f71883c
|
|
7
|
+
data.tar.gz: 79366a3af17543676a403b43b23e2df31b5c4219f2b440a2a438053ee8fa651f6513651e85b237693f30749afe085879d948c828a6090337c5e14508398ba7a3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,161 @@
|
|
|
1
|
+
## [0.6.2] - 2026-10-22
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **A single-section list message no longer needs a section title.** Meta requires
|
|
6
|
+
`sections[].title` only when a list has more than one section, but
|
|
7
|
+
`Interactive::ListButtons::Section` demanded one always — `title:` was a required
|
|
8
|
+
keyword with a presence validation — so a valid single-section list without a title
|
|
9
|
+
could not be built. `Section` now takes `title:` as optional (still max 24 characters)
|
|
10
|
+
and omits the key from the payload when it is absent, and `ListButtons` enforces Meta's
|
|
11
|
+
actual rule: with two or more sections, every one must have a title, otherwise it fails
|
|
12
|
+
with "Sections must each have a title when there is more than one section". A blank
|
|
13
|
+
title (`""` or whitespace) is treated as absent — omitted from a single section, and
|
|
14
|
+
counted as missing when there are several — rather than sent as an empty heading.
|
|
15
|
+
|
|
16
|
+
## [0.6.1] - 2026-10-21
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`Whatsapp::Schedule`** — `List` and `Create` wrap a WhatsApp Business Account's
|
|
21
|
+
campaign schedules (`/{waba_id}/schedules`): an approved template sent to an audience
|
|
22
|
+
at a Unix `delivery_time`. `Create` checks all six documented-required fields before
|
|
23
|
+
the request, and requires `delivery_time` to be an integer. It checks nothing else,
|
|
24
|
+
since Meta publishes no length limits and no rule that the time must be in the future.
|
|
25
|
+
`List` takes `fields`, `limit`, `after`, and `before`, and returns an `Enumerable`
|
|
26
|
+
`Response::Collection` of `Response::Node`s with `next_cursor`. Its paging reuses
|
|
27
|
+
`MessageTemplates::Response::Paging` rather than duplicating it. `Node#status` stays
|
|
28
|
+
the raw string, with `Statuses` constants and `scheduled?`/`sending?`/`completed?`/
|
|
29
|
+
`failed?` for comparison. Meta's default `fields` omit `id`, so
|
|
30
|
+
`List::Fields::ALL` is offered to ask for it. Every failure raises the new
|
|
31
|
+
`Whatsapp::Schedule::Error`.
|
|
32
|
+
|
|
33
|
+
## [0.6.0] - 2026-10-20
|
|
34
|
+
|
|
35
|
+
Completes the Business-Scoped User ID (BSUID) work that 0.5.0 began. 0.5.0 covered the
|
|
36
|
+
inbound webhook half; this covers the send side, the four webhook fields Meta's BSUID
|
|
37
|
+
documentation changed but 0.5.0 did not touch, the two new phone-number APIs, and the
|
|
38
|
+
button that asks a username adopter for their phone number back.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`Whatsapp::Webhook::UserPreferences` and `::History` could never populate.** Both read
|
|
43
|
+
their payload one nesting level too high — `value["wa_id"]` and `value["metadata"]`,
|
|
44
|
+
where Meta nests entries under a repeat of the field's own name
|
|
45
|
+
(`value["user_preferences"][]`, `value["history"][]`), the convention `UserIdUpdate`
|
|
46
|
+
already followed. Every accessor on both classes returned `nil` or `[]` for a real
|
|
47
|
+
payload. The BSUID documentation is the first published example for either field, which
|
|
48
|
+
is what caught it; both specs now carry those payloads verbatim as regression guards.
|
|
49
|
+
**Breaking:** `UserPreferences`' `wa_id`/`detail`/`value`/`timestamp` move to
|
|
50
|
+
`change.value.preferences.first.*`, and `History#threads` moves to
|
|
51
|
+
`chunks.first.threads` — `#phase`/`#chunk_order`/`#progress` keep their names and now
|
|
52
|
+
actually work.
|
|
53
|
+
- **Corrected `Status#recipient_user_id`'s documented behavior.** The docs and one spec
|
|
54
|
+
claimed it appears only when you addressed a message to a BSUID. Per Meta's May 4, 2026
|
|
55
|
+
correction it is sent on _every_ status however the message was addressed, and omitted
|
|
56
|
+
only on a `failed` status for a message sent to a phone number. The deserializer was
|
|
57
|
+
always right; the claim around it was not. Also newly documented: the status webhook's
|
|
58
|
+
`contacts[]` accompanies `sent`/`delivered`/`read` but is omitted entirely for `failed`,
|
|
59
|
+
and `contacts[].profile.username` is omitted on `sent`.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
|
|
63
|
+
- **Send to a BSUID.** `Whatsapp::Messages::Base` gains `recipient` alongside `to`, so a
|
|
64
|
+
BSUID read off a webhook can be replied to without ever knowing the phone number. Supply
|
|
65
|
+
either or both — Meta gives `to` precedence — and at least one is required, which is now
|
|
66
|
+
a validation rather than a missing-keyword `ArgumentError`. Because all 13 message kinds
|
|
67
|
+
already forwarded with `super(**)`, and the `KINDS` factory splats `**payload`, this
|
|
68
|
+
reached every kind and `send_<kind>!` method without touching either. `envelope` compacts
|
|
69
|
+
the recipient pair only, so an omitted identifier is left out rather than sent as null.
|
|
70
|
+
`Messages::Response::Contacts` gains `user_id` and makes `wa_id` optional to match —
|
|
71
|
+
noting that Meta omits `user_id` whenever the message went to a phone number, _including_
|
|
72
|
+
when both identifiers were supplied, so a nil `user_id` does not mean the recipient has
|
|
73
|
+
no BSUID. The BSUID is deliberately not format-checked: its canonical source is this
|
|
74
|
+
gem's own webhook side, which keeps it a raw string, and rejecting on the way out what
|
|
75
|
+
was accepted on the way in would be the worst possible asymmetry.
|
|
76
|
+
- **`Whatsapp::BusinessPhoneNumber::Username`** — `Get`, `Claim`, `Delete`, and `Suggest`
|
|
77
|
+
wrap the searchable `@name` a WhatsApp user can find a number by. Four endpoints, three
|
|
78
|
+
wire shapes, two new value classes: `Username::Details` covers both `Get` and `Claim`
|
|
79
|
+
since `{status}` is a subset of `{username, status}`, `Username::Suggestions` owns the
|
|
80
|
+
doubly-wrapped suggestions envelope and is `Enumerable`, and `Delete` reuses the existing
|
|
81
|
+
`BusinessPhoneNumber::Response` for its bare `{success}`. Every one of Meta's published
|
|
82
|
+
username format rules is checked before the request, since the edge is rate limited — bar
|
|
83
|
+
the domain-suffix rule, whose published list ends in "and so on", so a suffix outside the
|
|
84
|
+
documented ten is left to the API. Mixed case is downcased rather than rejected, because
|
|
85
|
+
Meta compares usernames case-insensitively. `Details::Statuses` deliberately omits
|
|
86
|
+
`"deleted"`: that value reaches you only through the `business_username_updates` webhook,
|
|
87
|
+
and a write-side enum must not name a state no endpoint here can return.
|
|
88
|
+
- **`Whatsapp::BusinessPhoneNumber::ContactBook::Delete`** — removes a stored user contact
|
|
89
|
+
from the store Meta uses to keep putting a username adopter's phone number in your
|
|
90
|
+
webhooks. Returns its own `ContactBook::Deletion` rather than the shared `Response`,
|
|
91
|
+
because `deleted` is a second independent boolean that changes how `success` reads:
|
|
92
|
+
`success: true, deleted: false` means the request was fine and there was simply nothing
|
|
93
|
+
stored. Rejects a parent BSUID locally — the one BSUID-shape assertion in the gem, and
|
|
94
|
+
safe because it tests for a documented extra segment rather than a country-code grammar
|
|
95
|
+
Meta has never published. Both new namespaces reuse `BusinessPhoneNumber::Transport`
|
|
96
|
+
unchanged; there are still only two transports in that directory, three sub-namespaces
|
|
97
|
+
later.
|
|
98
|
+
- **`REQUEST_CONTACT_INFO`**, in both forms: a `MessageTemplates::Button` type for opening
|
|
99
|
+
a conversation with the ask, and a `Messages::Interactive` action kind
|
|
100
|
+
(`type: :request_contact_info`) for making it inside the 24-hour service window. Both
|
|
101
|
+
were previously recorded as deliberately absent for want of a published schema; the BSUID
|
|
102
|
+
documentation publishes one. The template button's `UTILITY`/`MARKETING` restriction lives
|
|
103
|
+
in `ComponentSet` alongside the other cross-component rules, and is checked by drilling
|
|
104
|
+
into the `BUTTONS` component rather than through `CATEGORY_RESTRICTED_TYPES`, which keys
|
|
105
|
+
on component types. The interactive kind is the first with nothing for a caller to
|
|
106
|
+
supply, so `ACTION_TYPES` gained a `parameterless` flag and `action:` is no longer
|
|
107
|
+
required for it.
|
|
108
|
+
- **Four more webhook fields brought up to their published schemas.**
|
|
109
|
+
`Whatsapp::Webhook::SmbMessageEchoes` gains a value-level `contacts` array and wraps each
|
|
110
|
+
echo in a new `SmbMessageEchoes::Echo` carrying `to`/`to_user_id`/`to_parent_user_id` —
|
|
111
|
+
a wrapper rather than three more accessors on `Message::Base`, because an inbound message
|
|
112
|
+
can never carry a recipient, and three permanently-nil readers on fifteen classes would
|
|
113
|
+
turn a helpful `NoMethodError` into a silent `nil`. **Breaking:** `echo.body` becomes
|
|
114
|
+
`echo.message.body`. `SmbAppStateSync` gains `messaging_product`/`metadata` and types its
|
|
115
|
+
synced contact as `SmbAppStateSync::Contact` (`full_name`, `first_name`, `phone_number`,
|
|
116
|
+
`user_id`, `parent_user_id`, `username`), with the per-entry `metadata.timestamp`
|
|
117
|
+
flattened onto the entry. **Breaking:** `sync.contact["phone_number"]` becomes
|
|
118
|
+
`sync.contact.phone_number`, and an entry with no contact returns `nil` rather than `{}`.
|
|
119
|
+
All four fields, plus `UserPreferences` and `History`, now expose `raw`.
|
|
120
|
+
- **`revoke` and `edit` message types**, registered in `Message::MESSAGE_TYPES` rather than
|
|
121
|
+
falling through to `Message::Unknown`. `Revoke` carries `original_message_id`; `Edit`
|
|
122
|
+
carries that plus a nested `message` recursed back through `Message.deserialize`, so an
|
|
123
|
+
edited image behaves exactly like an inbound one — content only, so the nested payload's
|
|
124
|
+
own `from`/`id`/`timestamp` read `nil`. Both also arrive through `smb_message_echoes`
|
|
125
|
+
when the business edits or revokes from the WhatsApp Business app.
|
|
126
|
+
- **`Message::Base#group_id`**, set when an inbound message came from a group chat.
|
|
127
|
+
Extracted once in `common_attributes`, so every message type has it. The matching
|
|
128
|
+
group-status identifiers on `Status` are deliberately _not_ added, and are now recorded
|
|
129
|
+
under a new "Not implemented" section in `lib/ruby/whatsapp/webhook/CLAUDE.md` along with
|
|
130
|
+
the `calls` and `group_participants_update` fields: they only appear on a status for a
|
|
131
|
+
message sent to a group, and this gem wraps no Groups API, so it can never receive one.
|
|
132
|
+
|
|
133
|
+
## [0.5.0] - 2026-08-24
|
|
134
|
+
|
|
135
|
+
- Add Business-Scoped User ID (BSUID) support to inbound webhooks: Meta is rolling out
|
|
136
|
+
WhatsApp usernames, after which a user's phone number can be omitted from a webhook
|
|
137
|
+
entirely, leaving their BSUID (`user_id`, e.g. `"US.13491208655302741918"`) as the only
|
|
138
|
+
stable identifier. `Whatsapp::Webhook::Contact`, `Message::Base` (and therefore every
|
|
139
|
+
message type: `Text`, the `Media` family, `Location`, `Contacts`, `Interactive`,
|
|
140
|
+
`Button`, `Order`, `System`, `Reaction`, `Unknown`), and `Status` now all carry
|
|
141
|
+
`user_id`/`from_user_id`/`recipient_user_id`, alongside `parent_user_id` variants for
|
|
142
|
+
businesses enrolled in [parent BSUIDs](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids#parent-business-scoped-user-ids).
|
|
143
|
+
`Contact` additionally gains `username`. `Message::Contacts::Contact` gains `vcard`/
|
|
144
|
+
`origin` for cards shared via a `REQUEST_CONTACT_INFO` button, and `Message::System`
|
|
145
|
+
gains its own `user_id`/`parent_user_id` for the customer's new BSUID after a
|
|
146
|
+
`user_changed_user_id` event. `Message::Context` carries `from_user_id`/
|
|
147
|
+
`from_parent_user_id` too, so a quoted reply still identifies its sender once the phone
|
|
148
|
+
number is gone. `from_user_id`/`from_parent_user_id` are extracted once in
|
|
149
|
+
`Message::Base.common_attributes`, so every message type gained them without any
|
|
150
|
+
per-type changes.
|
|
151
|
+
- Add two new confirmed webhook fields from the same BSUID rollout:
|
|
152
|
+
`Whatsapp::Webhook::UserIdUpdate` (a user's BSUID changed — composes the existing
|
|
153
|
+
`Metadata`/`Contact` plus `Entry`/`IdChange` for the `{previous, current}` change
|
|
154
|
+
records) and `Whatsapp::Webhook::BusinessUsernameUpdates` (a business phone number's
|
|
155
|
+
username moved between `approved`/`reserved`/`deleted`). Both also expose `raw`, the
|
|
156
|
+
untouched `value` hash, so a payload whose shape differs from Meta's published example
|
|
157
|
+
is still recoverable rather than silently deserializing to an empty object.
|
|
158
|
+
|
|
1
159
|
## [0.4.2] - 2026-08-19
|
|
2
160
|
|
|
3
161
|
- Add WhatsApp Business Profile management: `Whatsapp::BusinessPhoneNumber::Profile::Get` reads the public card a WhatsApp user sees before they reply — about text, description, address, email, websites, industry vertical, and profile picture URL — and `::Profile::Update` writes them. Unlike `::Account`, this is not an exception to the module's `phone_id` rule: Meta exposes the profile as an edge on the phone number itself (`GET`/`POST /{phone_id}/whatsapp_business_profile`), so both actions reuse the existing `BusinessPhoneNumber::Transport#edge_path` unchanged — no new transport, no new ID to configure. `Update` validates the 21-value `Profile::Verticals` enum and Meta's documented limits (about 139, address 256, description 512, email 128 characters, at most 2 websites) before any request is made, and compacts omitted fields out of the body rather than sending null — which Meta reads as an instruction to blank them — with an empty `websites` array surviving on purpose, since that is how the list is cleared. `email`'s format is deliberately left to Meta, which validates it server-side; a regex here would reject valid exotic addresses. It reuses the existing `BusinessPhoneNumber::Response`, since the endpoint answers a bare `{success}`.
|
data/README.md
CHANGED
|
@@ -48,6 +48,7 @@ onboarding, and reading or updating the business account itself.
|
|
|
48
48
|
- [🔔 Webhooks](#-webhooks)
|
|
49
49
|
- [📷 Media](#-media)
|
|
50
50
|
- [🔌 Subscribed Apps](#-subscribed-apps)
|
|
51
|
+
- [📅 Scheduling Campaigns](#-scheduling-campaigns)
|
|
51
52
|
- [📞 Business Phone Numbers](#-business-phone-numbers)
|
|
52
53
|
- [🚨 Errors](#-errors)
|
|
53
54
|
- [🧩 Compatibility](#-compatibility)
|
|
@@ -69,9 +70,14 @@ onboarding, and reading or updating the business account itself.
|
|
|
69
70
|
- **Secrets stay out of logs.** `api_key`, `app_secret`, and `verify_token` are
|
|
70
71
|
redacted from every `#inspect`, including credentials in transit like a
|
|
71
72
|
registration PIN.
|
|
72
|
-
- **Inbound webhooks, fully typed.** An object tree for all
|
|
73
|
+
- **Inbound webhooks, fully typed.** An object tree for all 21 documented Meta
|
|
73
74
|
notification fields, plus HMAC signature verification and a Rails controller
|
|
74
75
|
generator.
|
|
76
|
+
- **Usernames and BSUIDs, both directions.** Once a WhatsApp user adopts a username,
|
|
77
|
+
their phone number can vanish from your payloads. Inbound notifications still identify
|
|
78
|
+
their sender through a Business-Scoped User ID, and `recipient:` sends straight back to
|
|
79
|
+
one — so a BSUID read off a webhook can be replied to without ever knowing the number.
|
|
80
|
+
Your own business username is managed too, along with Meta's contact book.
|
|
75
81
|
- **Persistent HTTP connections** via `HTTP.persistent`, reused across requests, with
|
|
76
82
|
pluggable logging.
|
|
77
83
|
- **Four runtime dependencies**, no Rails requirement. Rails integration activates
|
|
@@ -120,13 +126,16 @@ end
|
|
|
120
126
|
phone_id ──► Messages · Media · BusinessPhoneNumber
|
|
121
127
|
permission: whatsapp_business_messaging
|
|
122
128
|
|
|
123
|
-
waba_id ──► MessageTemplates · SubscribedApp · BusinessPhoneNumber::Account
|
|
129
|
+
waba_id ──► MessageTemplates · SubscribedApp · Schedule · BusinessPhoneNumber::Account
|
|
124
130
|
permission: whatsapp_business_management
|
|
125
131
|
```
|
|
126
132
|
|
|
127
133
|
`BusinessPhoneNumber` appears on both sides: its onboarding actions address a phone
|
|
128
134
|
number, its `Account` actions the account that number belongs to.
|
|
129
135
|
|
|
136
|
+
`Schedule`'s permission is an assumption: Meta's Schedules reference names none, so it
|
|
137
|
+
is grouped under `whatsapp_business_management` only because it addresses the WABA.
|
|
138
|
+
|
|
130
139
|
`api_key`, `app_secret`, and `verify_token` are redacted from `Configuration#inspect`
|
|
131
140
|
and `Client#inspect`, so they will not leak into logs or error reports.
|
|
132
141
|
|
|
@@ -155,9 +164,10 @@ response.contacts.first.wa_id # => "15551234567"
|
|
|
155
164
|
| [**Configuration**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/configuration.md) | Credentials, the client, connection reuse, instrumentation |
|
|
156
165
|
| [**Messages**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/README.md) | Every message kind, one page each, with exact payloads |
|
|
157
166
|
| [**Message Templates**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/message_templates/README.md) | Creating and managing templates: standard, auth, carousel, offers, library |
|
|
158
|
-
| [**Webhooks**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/webhooks/README.md) | Install, verification, signatures, and all
|
|
167
|
+
| [**Webhooks**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/webhooks/README.md) | Install, verification, signatures, and all 21 notification fields |
|
|
159
168
|
| [**Media**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/media/README.md) | Upload, download, delete, and the token-safety allowlist |
|
|
160
169
|
| [**Subscribed Apps**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/subscribed_app/README.md) | Turning webhook delivery on and off for an account |
|
|
170
|
+
| [**Campaign Schedules**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/schedule/README.md) | Sending an approved template to an audience at a set time |
|
|
161
171
|
| [**Business Phone Numbers**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/README.md) | Onboarding: request code → verify → register |
|
|
162
172
|
| [**Business Account**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/account.md) | Reading and updating the account: name, timezone, review status |
|
|
163
173
|
| [**Business Profile**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/profile.md) | The profile a user sees: about, description, address, vertical, picture |
|
|
@@ -167,7 +177,13 @@ Or start at the [documentation index](https://github.com/saleszera/ruby-whatsapp
|
|
|
167
177
|
|
|
168
178
|
## 💬 Sending Messages
|
|
169
179
|
|
|
170
|
-
Every registered kind gets its own `Whatsapp::Messages.send_<kind>!` class method
|
|
180
|
+
Every registered kind gets its own `Whatsapp::Messages.send_<kind>!` class method.
|
|
181
|
+
Address a recipient by phone number, by BSUID, or both — at least one is required:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi") # phone number
|
|
185
|
+
Whatsapp::Messages.send_text!(recipient: "US.13491208655302741918", body: "Hi") # BSUID
|
|
186
|
+
```
|
|
171
187
|
|
|
172
188
|
```ruby
|
|
173
189
|
Whatsapp::Messages.send_interactive!(
|
|
@@ -245,7 +261,7 @@ plus list, find, update, and delete.
|
|
|
245
261
|
|
|
246
262
|
## 🔔 Webhooks
|
|
247
263
|
|
|
248
|
-
Meta pushes inbound messages, delivery statuses, and
|
|
264
|
+
Meta pushes inbound messages, delivery statuses, and 20 other notification types to a
|
|
249
265
|
callback URL you register. Inside a Rails app:
|
|
250
266
|
|
|
251
267
|
```bash
|
|
@@ -282,8 +298,9 @@ message.id # => "wamid.HBg..."
|
|
|
282
298
|
message.body # => "Does it come in another color?"
|
|
283
299
|
```
|
|
284
300
|
|
|
285
|
-
All
|
|
286
|
-
|
|
301
|
+
All 21 documented fields get a class. `messages`, `user_id_update`, and
|
|
302
|
+
`business_username_updates`, `user_preferences`, and `history` have a Meta-published schema; the other 16 are best-effort
|
|
303
|
+
and flagged as such, per field.
|
|
287
304
|
|
|
288
305
|
→ **[Webhooks](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/webhooks/README.md)**
|
|
289
306
|
· [inbound messages & statuses](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/webhooks/messages.md)
|
|
@@ -321,6 +338,26 @@ Tech Providers routing several accounts to different callback URLs pass an
|
|
|
321
338
|
|
|
322
339
|
→ **[Subscribed apps](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/subscribed_app/README.md)**
|
|
323
340
|
|
|
341
|
+
## 📅 Scheduling Campaigns
|
|
342
|
+
|
|
343
|
+
Send an approved template to an audience at a set time, and list what is scheduled:
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
Whatsapp::Schedule::Create.call(
|
|
347
|
+
hsm_id: "1234567890", audience_id: "2345678901", waba_cs_id: "3456789012",
|
|
348
|
+
name: "Black Friday", description: "Black Friday promo to VIP customers",
|
|
349
|
+
delivery_time: Time.now.to_i + 3600
|
|
350
|
+
).id # => "9876543210"
|
|
351
|
+
|
|
352
|
+
page = Whatsapp::Schedule::List.call(fields: Whatsapp::Schedule::List::Fields::ALL, limit: 50)
|
|
353
|
+
page.select(&:scheduled?).map(&:name) # => ["Black Friday"]
|
|
354
|
+
Whatsapp::Schedule::List.call(after: page.next_cursor) if page.next_cursor
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Meta's default fields leave out `id`, so ask for it with `fields:` when you need it.
|
|
358
|
+
|
|
359
|
+
→ **[Campaign schedules](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/schedule/README.md)**
|
|
360
|
+
|
|
324
361
|
## 📞 Business Phone Numbers
|
|
325
362
|
|
|
326
363
|
A phone number is unusable with Cloud API until it is **registered** — the prerequisite
|
|
@@ -372,9 +409,34 @@ Whatsapp::BusinessPhoneNumber::Profile::Update.call(
|
|
|
372
409
|
Character limits, the 21-value vertical enum, and the two-website cap are all checked
|
|
373
410
|
before the request goes out.
|
|
374
411
|
|
|
412
|
+
The **business username** — the searchable `@name` a user can find you by — is managed on
|
|
413
|
+
the same `phone_id`:
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
Whatsapp::BusinessPhoneNumber::Username::Suggest.call.to_a
|
|
417
|
+
# => ["jaspersmarket", "jaspers_market"] — names Meta reserved for you
|
|
418
|
+
|
|
419
|
+
Whatsapp::BusinessPhoneNumber::Username::Claim.call(username: "jaspersmarket").reserved?
|
|
420
|
+
# => true, until Meta makes the feature generally available
|
|
421
|
+
|
|
422
|
+
Whatsapp::BusinessPhoneNumber::Username::Get.call.approved? # => false, for now
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Every one of Meta's format rules is checked locally first, so a name that was never going
|
|
426
|
+
to be accepted doesn't cost you a rate-limited attempt. And Meta's **contact book** — the
|
|
427
|
+
store that keeps putting a username adopter's phone number in your webhooks — can be
|
|
428
|
+
pruned:
|
|
429
|
+
|
|
430
|
+
```ruby
|
|
431
|
+
result = Whatsapp::BusinessPhoneNumber::ContactBook::Delete.call(bsuid: "US.13491208655302741918")
|
|
432
|
+
result.deleted # => false means there was simply nothing stored, not a failure
|
|
433
|
+
```
|
|
434
|
+
|
|
375
435
|
→ **[Business phone numbers](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/README.md)**
|
|
376
436
|
· [the business account](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/account.md)
|
|
377
437
|
· [the business profile](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/profile.md)
|
|
438
|
+
· [business usernames](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/username.md)
|
|
439
|
+
· [the contact book](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/contact_book.md)
|
|
378
440
|
|
|
379
441
|
## 🚨 Errors
|
|
380
442
|
|
|
@@ -388,6 +450,7 @@ you can rescue narrowly:
|
|
|
388
450
|
| `Whatsapp::Media::MediaError` | Anything in `Media` |
|
|
389
451
|
| `Whatsapp::MessageTemplates::TemplateError` | Anything in `MessageTemplates` |
|
|
390
452
|
| `Whatsapp::SubscribedApp::Error` | Anything in `SubscribedApp` |
|
|
453
|
+
| `Whatsapp::Schedule::Error` | Anything in `Schedule` |
|
|
391
454
|
| `Whatsapp::BusinessPhoneNumber::Error` | Anything in `BusinessPhoneNumber`, including `Account` |
|
|
392
455
|
|
|
393
456
|
Local validation failures raise `ActiveModel::ValidationError` instead — they happen at
|
data/docs/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Full reference for [ruby-whatsapp](https://github.com/saleszera/ruby-whatsapp).
|
|
|
17
17
|
| [messages/](messages/README.md) | Sending every message kind, the shared envelope, responses |
|
|
18
18
|
| [media/](media/README.md) | Upload, download, delete, and the token-safety allowlist |
|
|
19
19
|
| [message_templates/](message_templates/README.md) | Creating and managing the templates you send |
|
|
20
|
+
| [schedule/](schedule/README.md) | Scheduling an approved template to go out to an audience later |
|
|
20
21
|
|
|
21
22
|
### Message kinds
|
|
22
23
|
|
|
@@ -49,7 +50,7 @@ Full reference for [ruby-whatsapp](https://github.com/saleszera/ruby-whatsapp).
|
|
|
49
50
|
|
|
50
51
|
| Page | Covers |
|
|
51
52
|
| --- | --- |
|
|
52
|
-
| [webhooks/](webhooks/README.md) | Install, verification, signature, dispatch, all
|
|
53
|
+
| [webhooks/](webhooks/README.md) | Install, verification, signature, dispatch, all 21 notification fields |
|
|
53
54
|
| [webhooks/messages.md](webhooks/messages.md) | Inbound messages and delivery statuses — the one you'll actually use |
|
|
54
55
|
|
|
55
56
|
## Account administration
|
|
@@ -60,6 +61,8 @@ Full reference for [ruby-whatsapp](https://github.com/saleszera/ruby-whatsapp).
|
|
|
60
61
|
| [business_phone_number/](business_phone_number/README.md) | Onboarding a phone number: request code → verify → register |
|
|
61
62
|
| [business_phone_number/account.md](business_phone_number/account.md) | Reading and updating the business account itself |
|
|
62
63
|
| [business_phone_number/profile.md](business_phone_number/profile.md) | Reading and updating the business profile a user sees |
|
|
64
|
+
| [business_phone_number/username.md](business_phone_number/username.md) | Claiming and managing the searchable business username |
|
|
65
|
+
| [business_phone_number/contact_book.md](business_phone_number/contact_book.md) | Removing a stored user contact from Meta's contact book |
|
|
63
66
|
|
|
64
67
|
## Reading order
|
|
65
68
|
|
|
@@ -27,6 +27,11 @@ Addresses your **phone number** (`phone_id`, not `waba_id`), like
|
|
|
27
27
|
> and picture. Those address your phone number (`phone_id`), just like the four actions
|
|
28
28
|
> below.
|
|
29
29
|
|
|
30
|
+
> **Looking for the username?** [`Username::Get` / `Claim` / `Delete` / `Suggest`](username.md)
|
|
31
|
+
> read and set the searchable `@name` a WhatsApp user can find the number by, and
|
|
32
|
+
> [`ContactBook::Delete`](contact_book.md) forgets a stored user contact. Both address
|
|
33
|
+
> `phone_id`, like the four actions below.
|
|
34
|
+
|
|
30
35
|
> **Unrelated to [`SubscribedApp`](../subscribed_app/README.md).** That turns *webhook
|
|
31
36
|
> delivery* on and off for a whole WhatsApp Business Account. This is per phone number
|
|
32
37
|
> and has nothing to do with notifications.
|
|
@@ -55,6 +60,11 @@ Whatsapp::BusinessPhoneNumber::Deregister.call
|
|
|
55
60
|
| [`Account::Update.call(name:, timezone_id:)`](account.md) | `POST /{waba_id}` | `Response` |
|
|
56
61
|
| [`Profile::Get.call(fields:)`](profile.md) | `GET /{phone_id}/whatsapp_business_profile` | `Profile::Details` |
|
|
57
62
|
| [`Profile::Update.call(**fields)`](profile.md) | `POST /{phone_id}/whatsapp_business_profile` | `Response` |
|
|
63
|
+
| [`Username::Get.call`](username.md) | `GET /{phone_id}/username` | `Username::Details` |
|
|
64
|
+
| [`Username::Claim.call(username:, transfer_action:)`](username.md) | `POST /{phone_id}/username` | `Username::Details` |
|
|
65
|
+
| [`Username::Delete.call`](username.md) | `DELETE /{phone_id}/username` | `Response` |
|
|
66
|
+
| [`Username::Suggest.call`](username.md) | `GET /{phone_id}/username_suggestions` | `Username::Suggestions` |
|
|
67
|
+
| [`ContactBook::Delete.call(bsuid:)`](contact_book.md) | `DELETE /{phone_id}/contact_book` | `ContactBook::Deletion` |
|
|
58
68
|
|
|
59
69
|
Every action accepts an optional `client:` and raises
|
|
60
70
|
`Whatsapp::BusinessPhoneNumber::Error` if `phone_id` is missing or the API rejects the
|
|
@@ -221,7 +231,8 @@ Local validation failures raise `ActiveModel::ValidationError` before any reques
|
|
|
221
231
|
---
|
|
222
232
|
|
|
223
233
|
**See also:** [Reading and updating the business account](account.md) ·
|
|
224
|
-
[Reading and updating the business profile](profile.md)
|
|
234
|
+
[Reading and updating the business profile](profile.md) ·
|
|
235
|
+
[Business usernames](username.md) · [Contact book](contact_book.md)
|
|
225
236
|
|
|
226
237
|
---
|
|
227
238
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Contact book
|
|
2
|
+
|
|
3
|
+
Meta keeps a **contact book** for your business portfolio: the phone numbers and BSUIDs
|
|
4
|
+
of users you have interacted with. Its purpose is thread continuity — once a user adopts
|
|
5
|
+
a username, Meta will keep putting their phone number in your webhooks if they are in
|
|
6
|
+
your contact book, instead of leaving you with only their
|
|
7
|
+
[BSUID](../webhooks/messages.md#business-scoped-user-ids-bsuid).
|
|
8
|
+
|
|
9
|
+
The store itself is hosted by Meta and needs no integration work; entries appear on their
|
|
10
|
+
own as you exchange messages or calls, and when a user taps a `REQUEST_CONTACT_INFO`
|
|
11
|
+
button (which arrives as a [`contacts` message](../webhooks/messages.md#contacts) with
|
|
12
|
+
`origin == "contact_request"`). This page covers the one operation Meta exposes on it:
|
|
13
|
+
removing an entry.
|
|
14
|
+
|
|
15
|
+
| Method | Request | Returns |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `ContactBook::Delete.call(bsuid:)` | `DELETE /{phone_id}/contact_book` | `ContactBook::Deletion` |
|
|
18
|
+
|
|
19
|
+
## Deleting an entry
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
result = Whatsapp::BusinessPhoneNumber::ContactBook::Delete.call(bsuid: "US.13491208655302741918")
|
|
23
|
+
|
|
24
|
+
result.success # => true — the request was processed
|
|
25
|
+
result.deleted # => true — an entry existed and is gone
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The two booleans are independent, which is why this returns its own class rather than the
|
|
29
|
+
shared `Response`:
|
|
30
|
+
|
|
31
|
+
| `success` | `deleted` | Meaning |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `true` | `true` | An entry existed and was removed |
|
|
34
|
+
| `true` | `false` | The request was fine; there was nothing stored for that BSUID |
|
|
35
|
+
| `false` | — | The request was not processed |
|
|
36
|
+
|
|
37
|
+
So `deleted == false` is **not** a failure. Check `success` for that, or rescue
|
|
38
|
+
`Whatsapp::BusinessPhoneNumber::Error`.
|
|
39
|
+
|
|
40
|
+
## What it affects
|
|
41
|
+
|
|
42
|
+
> **The edge is on one phone number; the effect is portfolio-wide.** Contact books are
|
|
43
|
+
> scoped to the business portfolio, so deleting an entry removes that user's phone number
|
|
44
|
+
> from webhooks for **every** business phone number in the portfolio — not just the one
|
|
45
|
+
> you addressed.
|
|
46
|
+
|
|
47
|
+
And the consequence to expect: after a deletion, `wa_id`/`from` may start being omitted
|
|
48
|
+
from your webhooks for that user, leaving their BSUID as the only identifier. That is the
|
|
49
|
+
feature working, not a bug — but code that keys off `wa_id` will notice.
|
|
50
|
+
|
|
51
|
+
The phone number can come back, without a new deletion, if the user messages or calls you
|
|
52
|
+
again (or you them) within the 30-day window Meta caches, since that recreates the entry.
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
- **Standard BSUIDs only.** Parent BSUIDs are not supported. Since this release hands you
|
|
57
|
+
`user_id` and `parent_user_id` side by side on half a dozen webhook classes, the mix-up
|
|
58
|
+
is easy, so it is caught locally:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
Whatsapp::BusinessPhoneNumber::ContactBook::Delete.call(bsuid: "US.ENT.11815799212886844830")
|
|
62
|
+
# => ActiveModel::ValidationError: Bsuid looks like a parent BSUID;
|
|
63
|
+
# the contact book accepts standard BSUIDs only
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Detected by segment count rather than by matching `ENT`, which would bet that `ENT` is
|
|
67
|
+
the only marker Meta will ever use.
|
|
68
|
+
|
|
69
|
+
- **Same portfolio.** The BSUID must belong to the same portfolio as the phone number.
|
|
70
|
+
That is account state the gem cannot see, so it is left to the API — a mismatch comes
|
|
71
|
+
back as `Whatsapp::BusinessPhoneNumber::Error`.
|
|
72
|
+
|
|
73
|
+
## Disabling the feature entirely
|
|
74
|
+
|
|
75
|
+
The contact book can be turned off in **Meta Business Suite → Business settings →
|
|
76
|
+
Business info**. Disabling it stops all storage and deletes everything already stored;
|
|
77
|
+
re-enabling starts fresh, with no way to restore the old contents. There is no API for
|
|
78
|
+
this, so it is not wrapped.
|
|
79
|
+
|
|
80
|
+
## Not wrapped
|
|
81
|
+
|
|
82
|
+
Meta documents only the delete on this edge. A future read or write belongs in this same
|
|
83
|
+
namespace.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
**See also:** [Business usernames](username.md) ·
|
|
88
|
+
[BSUIDs in webhooks](../webhooks/messages.md#business-scoped-user-ids-bsuid)
|
|
89
|
+
|
|
90
|
+
**Meta docs:** https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids
|
|
@@ -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
|