ruby-whatsapp 0.4.2 → 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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +70 -7
  4. data/docs/README.md +4 -1
  5. data/docs/business_phone_number/README.md +12 -1
  6. data/docs/business_phone_number/contact_book.md +90 -0
  7. data/docs/business_phone_number/username.md +157 -0
  8. data/docs/configuration.md +6 -1
  9. data/docs/errors.md +12 -1
  10. data/docs/message_templates/README.md +1 -1
  11. data/docs/message_templates/components.md +37 -1
  12. data/docs/messages/README.md +86 -9
  13. data/docs/messages/address.md +1 -1
  14. data/docs/messages/audio.md +1 -1
  15. data/docs/messages/contacts.md +1 -1
  16. data/docs/messages/document.md +1 -1
  17. data/docs/messages/image.md +1 -1
  18. data/docs/messages/interactive.md +64 -3
  19. data/docs/messages/location.md +1 -1
  20. data/docs/messages/location_request.md +1 -1
  21. data/docs/messages/reaction.md +1 -1
  22. data/docs/messages/sticker.md +1 -1
  23. data/docs/messages/template.md +28 -1
  24. data/docs/messages/text.md +1 -1
  25. data/docs/messages/video.md +1 -1
  26. data/docs/schedule/README.md +183 -0
  27. data/docs/webhooks/README.md +20 -12
  28. data/docs/webhooks/business_username_updates.md +55 -0
  29. data/docs/webhooks/history.md +78 -15
  30. data/docs/webhooks/messages.md +141 -6
  31. data/docs/webhooks/smb_app_state_sync.md +50 -12
  32. data/docs/webhooks/smb_message_echoes.md +58 -21
  33. data/docs/webhooks/user_id_update.md +84 -0
  34. data/docs/webhooks/user_preferences.md +55 -16
  35. data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
  36. data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
  37. data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
  38. data/lib/ruby/whatsapp/business_phone_number/transport.rb +6 -5
  39. data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
  40. data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
  41. data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
  42. data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
  43. data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
  44. data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
  45. data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
  46. data/lib/ruby/whatsapp/business_phone_number.rb +9 -0
  47. data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
  48. data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
  49. data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
  50. data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
  51. data/lib/ruby/whatsapp/messages/base.rb +47 -5
  52. data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
  53. data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
  54. data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
  55. data/lib/ruby/whatsapp/schedule/create.rb +101 -0
  56. data/lib/ruby/whatsapp/schedule/list.rb +50 -0
  57. data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
  58. data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
  59. data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
  60. data/lib/ruby/whatsapp/schedule/response.rb +18 -0
  61. data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
  62. data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
  63. data/lib/ruby/whatsapp/schedule.rb +15 -0
  64. data/lib/ruby/whatsapp/version.rb +1 -1
  65. data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
  66. data/lib/ruby/whatsapp/webhook/change.rb +3 -1
  67. data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
  68. data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
  69. data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
  70. data/lib/ruby/whatsapp/webhook/history.rb +73 -34
  71. data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
  72. data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
  73. data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
  74. data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
  75. data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
  76. data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
  77. data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
  78. data/lib/ruby/whatsapp/webhook/message.rb +3 -0
  79. data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
  80. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
  81. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
  82. data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
  83. data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
  84. data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
  85. data/lib/ruby/whatsapp/webhook/status.rb +23 -2
  86. data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
  87. data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
  88. data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
  89. data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
  90. data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
  91. metadata +38 -1
data/docs/errors.md CHANGED
@@ -22,6 +22,7 @@ StandardError
22
22
  ├── Whatsapp::Media::MediaError any Media failure
23
23
  ├── Whatsapp::MessageTemplates::TemplateError any template-management failure
24
24
  ├── Whatsapp::SubscribedApp::Error any subscribed-apps failure
25
+ ├── Whatsapp::Schedule::Error any campaign-schedule failure
25
26
  └── Whatsapp::BusinessPhoneNumber::Error any phone-number-onboarding or account failure
26
27
  ```
27
28
 
@@ -85,9 +86,19 @@ end
85
86
  A required Ruby keyword that isn't supplied raises `ArgumentError`, not a validation
86
87
  error — it never reaches the validator:
87
88
 
89
+ ```ruby
90
+ Whatsapp::Messages.send_text!(to: "+15551234567")
91
+ # => ArgumentError: missing keyword: :body
92
+ ```
93
+
94
+ The recipient is the exception. Since a message can be addressed by phone number
95
+ (`to:`) *or* by [BSUID](messages/README.md#addressing-to-recipient-or-both)
96
+ (`recipient:`), neither keyword is individually required, so omitting both is a
97
+ validation failure rather than an arity one:
98
+
88
99
  ```ruby
89
100
  Whatsapp::Messages.send_text!(body: "Hi")
90
- # => ArgumentError: missing keyword: :to
101
+ # => ActiveModel::ValidationError: at least one of to or recipient must be provided
91
102
  ```
92
103
 
93
104
  ## API failures
@@ -191,7 +191,7 @@ templates.delete(hsm_ids: [], name: "x")
191
191
  [`Media#upload`](../media/README.md), whose media IDs are for *sending*, not
192
192
  template creation.
193
193
  - **Undocumented button types:** `FLOW`, `MPM`, `CATALOG`, `VOICE_CALL`, `VIDEO_CALL`,
194
- `POSTBACK`, `BOOKING_STATUS`, `PAYMENT_REQUEST`, `REQUEST_CONTACT_INFO`. Named in
194
+ `POSTBACK`, `BOOKING_STATUS`, `PAYMENT_REQUEST`. Named in
195
195
  Meta's Graph enum with no published field reference.
196
196
  [`LibraryTemplate`](library.md) does accept most of them, because Meta documents them
197
197
  by name in *that* context.
@@ -108,6 +108,7 @@ Resolved through the frozen `Button::TYPES` registry.
108
108
  | `:phone_number` | `PHONE_NUMBER` | `text` ≤ 25, `phone_number` ≤ **20** |
109
109
  | `:copy_code` | `COPY_CODE` | `example` ≤ **20** (a bare String), **no `text`** |
110
110
  | `:otp` | `OTP` | `otp_type`, `supported_apps`, `zero_tap_terms_accepted` |
111
+ | `:request_contact_info` | `REQUEST_CONTACT_INFO` | **no fields** — `text` optional and only ever `"Share Contact Info"` |
111
112
 
112
113
  ```ruby
113
114
  { type: :quick_reply, text: "More like this" }
@@ -115,6 +116,7 @@ Resolved through the frozen `Button::TYPES` registry.
115
116
  { type: :phone_number, text: "Call", phone_number: "15550051310" }
116
117
  { type: :copy_code, example: "SPRING25" }
117
118
  { type: :otp, otp_type: "COPY_CODE" }
119
+ { type: :request_contact_info }
118
120
  ```
119
121
 
120
122
  ### The URL button's single trailing variable
@@ -146,6 +148,39 @@ those labels:
146
148
  # Meta supplies the label
147
149
  ```
148
150
 
151
+ ### The request-contact-info button
152
+
153
+ Asks the recipient to share their phone number, which they hand over with one tap. It
154
+ takes nothing:
155
+
156
+ ```ruby
157
+ { type: :buttons, buttons: [{ type: :request_contact_info }] }
158
+ ```
159
+
160
+ This is the counterpart to Meta's usernames rollout: once a user adopts a username their
161
+ phone number may stop reaching you, leaving only their
162
+ [BSUID](../webhooks/messages.md#business-scoped-user-ids-bsuid). It is also the only way
163
+ to reach such a user with a one-tap, zero-tap, or copy-code
164
+ [authentication template](authentication.md), since those require a phone number.
165
+
166
+ Three things about it:
167
+
168
+ - **The label is not customizable.** Meta renders it in the recipient's own app language.
169
+ A `text` field is accepted but must be exactly `"Share Contact Info"`, so the simplest
170
+ correct thing is to omit it.
171
+ - **`UTILITY` and `MARKETING` only** — a cross-component rule, listed below.
172
+ - **Nothing to fill in when sending.** Unlike a URL or copy-code button, it takes no
173
+ parameter values, so a template carrying one needs no `button` component in the
174
+ [send payload](../messages/template.md) at all.
175
+
176
+ A tap arrives as an inbound [`contacts` message](../webhooks/messages.md#contacts) whose
177
+ `origin` is `"contact_request"`. There is also an
178
+ [interactive-message form](../messages/interactive.md) of the same button, for use inside
179
+ the 24-hour service window.
180
+
181
+ No per-type cap is enforced: Meta publishes none, and inventing one would reject valid
182
+ payloads — the same reason `OTP` is uncapped. The overall 10-button limit still applies.
183
+
149
184
  ## Cross-component rules
150
185
 
151
186
  Owned by `ComponentSet`, because it is the only object that can see the siblings:
@@ -155,6 +190,7 @@ Owned by `ComponentSet`, because it is the only object that can see the siblings
155
190
  `LIMITED_TIME_OFFER`.
156
191
  - `CAROUSEL` or `LIMITED_TIME_OFFER` → category must be `MARKETING`.
157
192
  - A `LOCATION` header → category must be `UTILITY` or `MARKETING`.
193
+ - A `REQUEST_CONTACT_INFO` button → category must be `UTILITY` or `MARKETING`.
158
194
  - `LIMITED_TIME_OFFER` present → `FOOTER` forbidden, `BODY` ≤ **600**, header
159
195
  `IMAGE`/`VIDEO` only, copy-code `example` ≤ **15** and that button must be **first**.
160
196
 
@@ -170,7 +206,7 @@ enum but deliberately absent:
170
206
  - **Components:** `GREETING`, `ALBUM`, `CALL_PERMISSION_REQUEST`,
171
207
  `TAP_TARGET_CONFIGURATION`, `ATTACHMENT`.
172
208
  - **Buttons:** `FLOW`, `MPM`, `CATALOG`, `VOICE_CALL`, `VIDEO_CALL`, `POSTBACK`,
173
- `BOOKING_STATUS`, `PAYMENT_REQUEST`, `REQUEST_CONTACT_INFO`. Most of these *are*
209
+ `BOOKING_STATUS`, `PAYMENT_REQUEST`. Most of these *are*
174
210
  accepted by [`LibraryTemplate`](library.md), because Meta documents them by name in
175
211
  that context.
176
212
 
@@ -61,9 +61,8 @@ Plus one endpoint that isn't a message kind at all:
61
61
 
62
62
  ## The shared envelope
63
63
 
64
- Every kind except `mark_message_as_read` inherits `Messages::Base`, which supplies
65
- `to` (the only universally required field, validated for presence) and wraps the
66
- kind-specific hash in a common envelope:
64
+ Every kind except `mark_message_as_read` inherits `Messages::Base`, which supplies the
65
+ recipient and wraps the kind-specific hash in a common envelope:
67
66
 
68
67
  ```ruby
69
68
  {
@@ -75,6 +74,56 @@ kind-specific hash in a common envelope:
75
74
  }
76
75
  ```
77
76
 
77
+ ### Addressing: `to`, `recipient`, or both
78
+
79
+ There are two ways to name a recipient, and **at least one is required**:
80
+
81
+ | Keyword | Value |
82
+ | --- | --- |
83
+ | `to` | The recipient's phone number |
84
+ | `recipient` | Their [business-scoped user ID](#business-scoped-user-ids-bsuid) (BSUID), or parent BSUID |
85
+
86
+ ```ruby
87
+ Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi") # phone number
88
+ Whatsapp::Messages.send_text!(recipient: "US.13491208655302741918", body: "Hi") # BSUID
89
+ ```
90
+
91
+ Pass both and Meta gives `to` precedence, answering as though `recipient` had not been
92
+ sent. Pass neither and you get an `ActiveModel::ValidationError` before any request:
93
+
94
+ ```ruby
95
+ Whatsapp::Messages.send_text!(body: "Hi")
96
+ # => ActiveModel::ValidationError: at least one of to or recipient must be provided
97
+ ```
98
+
99
+ Whichever you omit is left out of the payload entirely, never sent as `null`.
100
+
101
+ #### Business-scoped user IDs (BSUID)
102
+
103
+ Once a WhatsApp user adopts a username, their phone number can stop appearing in your
104
+ webhooks — and their BSUID becomes the only identifier you have. `recipient` is how you
105
+ reply to them. A BSUID is a country code, a period, and up to 128 alphanumeric
106
+ characters (`"US.13491208655302741918"`); a
107
+ [parent BSUID](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids#parent-business-scoped-user-ids),
108
+ usable across enrolled business portfolios, carries an extra `ENT.` segment
109
+ (`"US.ENT.11815799212886844830"`). Pass the whole value — dropping or altering any part
110
+ of it fails the request.
111
+
112
+ The gem does **not** format-check it. Meta describes the shape but publishes no
113
+ client-side rule, and the value you pass here normally comes straight off the
114
+ [inbound webhook side](../webhooks/messages.md#business-scoped-user-ids-bsuid), which
115
+ keeps it as a raw string. Replying to a customer Meta just told you about is exactly the
116
+ case a local regex would break.
117
+
118
+ ```ruby
119
+ # In your webhook handler, replying to whoever wrote in
120
+ contact = change.value.contacts.first
121
+ Whatsapp::Messages.send_text!(recipient: contact.user_id, body: "Thanks for reaching out!")
122
+ ```
123
+
124
+ BSUIDs are scoped to a business portfolio: a BSUID from one portfolio cannot be
125
+ messaged from a phone number owned by another.
126
+
78
127
  Three kinds break the `type == kind` symmetry because Meta transports them as
79
128
  interactive messages: [`address`](address.md), [`location_request`](location_request.md),
80
129
  and of course [`interactive`](interactive.md) all serialize with `type: "interactive"`.
@@ -95,13 +144,38 @@ body shaped like:
95
144
  ```ruby
96
145
  response = Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi")
97
146
 
98
- response.messaging_product # => "whatsapp"
99
- response.messages.first.id # => "wamid.HBgLMTU1NTU1NTU1NTUV..."
100
- response.contacts.first.input # => "+15551234567"
101
- response.contacts.first.wa_id # => "15551234567"
102
- response.success # => nil for a normal send
147
+ response.messaging_product # => "whatsapp"
148
+ response.messages.first.id # => "wamid.HBgLMTU1NTU1NTU1NTUV..."
149
+ response.contacts.first.input # => "+15551234567"
150
+ response.contacts.first.wa_id # => "15551234567"
151
+ response.contacts.first.user_id # => nil — see below
152
+ response.success # => nil for a normal send
153
+ ```
154
+
155
+ Which identifiers come back mirrors how you addressed the message:
156
+
157
+ | Accessor | Value |
158
+ | --- | --- |
159
+ | `input` | Exactly what you sent: the phone number, the BSUID or parent BSUID, or a group ID |
160
+ | `wa_id` | The phone number — **only** if you addressed it by phone number |
161
+ | `user_id` | The BSUID or parent BSUID — **only** if you addressed it by BSUID |
162
+
163
+ ```ruby
164
+ response = Whatsapp::Messages.send_text!(recipient: "US.13491208655302741918", body: "Hi")
165
+
166
+ response.contacts.first.input # => "US.13491208655302741918"
167
+ response.contacts.first.user_id # => "US.13491208655302741918"
168
+ response.contacts.first.wa_id # => nil
103
169
  ```
104
170
 
171
+ > **The precedence trap.** Send both `to` and `recipient` and `user_id` comes back
172
+ > **omitted**, because `to` wins and Meta answers as though you had only sent that. So
173
+ > `user_id` being `nil` does not mean the recipient has no BSUID — and code that
174
+ > correlates a send by reading `contacts.first.user_id` must not also pass `to:`.
175
+
176
+ Unlike the webhook classes, which separate `user_id` from `parent_user_id`, the send
177
+ response returns either kind in the single `user_id` field.
178
+
105
179
  `#success` is `nil` for a normal message send and `true`/`false` only for the
106
180
  status-update endpoints that reply `{"success": true}` — today just
107
181
  [`mark_message_as_read!`](mark_message_as_read.md).
@@ -110,7 +184,7 @@ status-update endpoints that reply `{"success": true}` — today just
110
184
 
111
185
  | Raised | When |
112
186
  | --- | --- |
113
- | `ArgumentError` | `to:` (or another required keyword) is missing |
187
+ | `ArgumentError` | A required keyword is missing (the recipient is a *validation*, not an arity, concern — see below) |
114
188
  | `ActiveModel::ValidationError` | A field fails a local validation |
115
189
  | `Whatsapp::Messages::PayloadError` | `kind:` is unrecognized — only reachable through the `Messages.new(kind:, payload:)` form |
116
190
  | `Whatsapp::RequestError` | The API answered with a non-2xx status |
@@ -122,6 +196,9 @@ field raises at *construction* time, before a client is ever touched:
122
196
  Whatsapp::Messages.send_image!(to: "+15551234567")
123
197
  # => ActiveModel::ValidationError: Either id or link must be present
124
198
 
199
+ Whatsapp::Messages.send_text!(body: "Hi")
200
+ # => ActiveModel::ValidationError: at least one of to or recipient must be provided
201
+
125
202
  Whatsapp::Messages.new(kind: :telepathy, payload: { to: "+1" })
126
203
  # => Whatsapp::Messages::PayloadError: Unknown message kind: :telepathy.
127
204
  # Known kinds: text, image, audio, video, document, sticker, contacts,
@@ -20,7 +20,7 @@ Whatsapp::Messages.send_address!(
20
20
 
21
21
  | Field | Required | Rules |
22
22
  | --- | --- | --- |
23
- | `to` | yes | The recipient's phone number |
23
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
24
24
  | `body` | yes | The prompt shown above the form |
25
25
  | `country` | yes | `"IN"` or `"SG"` only |
26
26
  | `footer` | no | Small print under the form |
@@ -10,7 +10,7 @@ Whatsapp::Messages.send_audio!(to: "+15551234567", link: "https://example.com/cl
10
10
 
11
11
  | Field | Required | Rules |
12
12
  | --- | --- | --- |
13
- | `to` | yes | The recipient's phone number |
13
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
14
14
  | `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
15
15
  | `link` | one of | A publicly reachable HTTPS URL |
16
16
 
@@ -15,7 +15,7 @@ Whatsapp::Messages.send_contacts!(
15
15
 
16
16
  | Field | Required | Rules |
17
17
  | --- | --- | --- |
18
- | `to` | yes | The recipient's phone number |
18
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
19
19
  | `contacts` | yes | An array containing **exactly one** contact |
20
20
 
21
21
  > **Exactly one contact.** The Cloud API currently accepts a single contact per
@@ -15,7 +15,7 @@ Whatsapp::Messages.send_document!(
15
15
 
16
16
  | Field | Required | Rules |
17
17
  | --- | --- | --- |
18
- | `to` | yes | The recipient's phone number |
18
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
19
19
  | `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
20
20
  | `link` | one of | A publicly reachable HTTPS URL |
21
21
  | `caption` | no | Max **1024** characters |
@@ -14,7 +14,7 @@ Whatsapp::Messages.send_image!(
14
14
 
15
15
  | Field | Required | Rules |
16
16
  | --- | --- | --- |
17
- | `to` | yes | The recipient's phone number |
17
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
18
18
  | `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
19
19
  | `link` | one of | A publicly reachable HTTPS URL |
20
20
  | `caption` | no | Max **1024** characters |
@@ -14,15 +14,16 @@ Unlike every other kind, `Interactive` is a **single class** that dispatches on
14
14
  | `:url_button` | `"cta_url"` | [A link rendered as a button](#cta-url-button) |
15
15
  | `:media_carousel` | `"carousel"` | [2–10 swipeable media cards](#media-carousel) |
16
16
  | `:product_carousel` | `"product_list"` | [2–10 catalog product cards](#product-carousel) |
17
+ | `:request_contact_info` | `"request_contact_info"` | [A one-tap "share my number" button](#request-contact-info) |
17
18
 
18
19
  ## Shared fields
19
20
 
20
21
  | Field | Required | Rules |
21
22
  | --- | --- | --- |
22
- | `to` | yes | The recipient's phone number |
23
- | `type` | yes | One of the five keys above (Symbol or String) |
23
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
24
+ | `type` | yes | One of the six keys above (Symbol or String) |
24
25
  | `body` | yes | Body text, max **1024** characters |
25
- | `action` | yes | Shape depends on `type` — see each section |
26
+ | `action` | usually | Shape depends on `type` — see each section. Omit it only for `:request_contact_info`, the one kind with nothing to supply |
26
27
  | `header` | no | `{ type: "text"\|"image"\|"video"\|"document", ... }` |
27
28
  | `footer` | no | `{ text: "..." }`, max **60** characters |
28
29
 
@@ -253,6 +254,66 @@ server-side.
253
254
 
254
255
  ---
255
256
 
257
+ ## Request contact info
258
+
259
+ Asks the recipient to share their phone number, which they hand over with one tap. The
260
+ one action kind that takes no `action:` — there is nothing to configure:
261
+
262
+ ```ruby
263
+ Whatsapp::Messages.send_interactive!(
264
+ recipient: "US.13491208655302741918",
265
+ type: :request_contact_info,
266
+ body: "Mind sharing your number so we can text you the confirmation?"
267
+ )
268
+ ```
269
+
270
+ ```json
271
+ {
272
+ "messaging_product": "whatsapp",
273
+ "recipient_type": "individual",
274
+ "recipient": "US.13491208655302741918",
275
+ "type": "interactive",
276
+ "interactive": {
277
+ "type": "request_contact_info",
278
+ "body": { "text": "Mind sharing your number so we can text you the confirmation?" },
279
+ "action": { "name": "request_contact_info" }
280
+ }
281
+ }
282
+ ```
283
+
284
+ Note `interactive.type` and `action.name` are the same literal — this is the only kind
285
+ where the registry key and the wire value coincide.
286
+
287
+ Use this inside the 24-hour service window; to *open* a conversation with the same ask,
288
+ use the [`REQUEST_CONTACT_INFO` template button](../message_templates/components.md#the-request-contact-info-button)
289
+ instead.
290
+
291
+ Why you would: once a WhatsApp user adopts a username, their phone number can stop
292
+ appearing in your webhooks, leaving only their
293
+ [BSUID](README.md#business-scoped-user-ids-bsuid) — which is enough to keep messaging
294
+ them, but not enough for a one-tap, zero-tap, or copy-code
295
+ [authentication template](../message_templates/authentication.md). This is how you ask
296
+ for the number back.
297
+
298
+ A tap arrives as an inbound [`contacts` message](../webhooks/messages.md#contacts) whose
299
+ `origin` is `"contact_request"`:
300
+
301
+ ```ruby
302
+ when "messages"
303
+ change.value.messages.each do |message|
304
+ next unless message.type == "contacts"
305
+
306
+ shared = message.contacts.first
307
+ next unless shared.origin == "contact_request"
308
+
309
+ Customer.find_by(bsuid: message.from_user_id)
310
+ &.update!(wa_id: shared.phones.first&.wa_id)
311
+ end
312
+ ```
313
+
314
+ Meta's published example shows no `header` or `footer`; both are accepted here and
315
+ omitted from the payload when absent, as with every other kind.
316
+
256
317
  ## Validation errors
257
318
 
258
319
  `Interactive` itself validates `type`, `body`, and `action` presence at construction.
@@ -17,7 +17,7 @@ Whatsapp::Messages.send_location!(
17
17
 
18
18
  | Field | Required | Rules |
19
19
  | --- | --- | --- |
20
- | `to` | yes | The recipient's phone number |
20
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
21
21
  | `latitude` | yes | Numeric |
22
22
  | `longitude` | yes | Numeric |
23
23
  | `name` | no | Label shown above the pin |
@@ -15,7 +15,7 @@ Whatsapp::Messages.send_location_request!(
15
15
 
16
16
  | Field | Required | Rules |
17
17
  | --- | --- | --- |
18
- | `to` | yes | The recipient's phone number |
18
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
19
19
  | `body` | yes | The prompt text, max **1024** characters |
20
20
 
21
21
  ## Serialized payload
@@ -11,7 +11,7 @@ Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg..."
11
11
 
12
12
  | Field | Required | Rules |
13
13
  | --- | --- | --- |
14
- | `to` | yes | The recipient's phone number |
14
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
15
15
  | `message_id` | yes | The WAMID of the message being reacted to |
16
16
  | `emoji` | yes | A single emoji, or `""` to remove |
17
17
 
@@ -10,7 +10,7 @@ Whatsapp::Messages.send_sticker!(to: "+15551234567", id: "STICKER_MEDIA_ID")
10
10
 
11
11
  | Field | Required | Rules |
12
12
  | --- | --- | --- |
13
- | `to` | yes | The recipient's phone number |
13
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
14
14
  | `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
15
15
  | `link` | one of | A publicly reachable HTTPS URL |
16
16
 
@@ -23,7 +23,7 @@ Whatsapp::Messages.send_template!(
23
23
 
24
24
  | Field | Required | Rules |
25
25
  | --- | --- | --- |
26
- | `to` | yes | The recipient's phone number |
26
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
27
27
  | `name` | yes | The approved template's name |
28
28
  | `language` | yes | `{ code: "en_US" }` — validated against the supported list |
29
29
  | `components` | no | Array of components supplying placeholder values. Defaults to `[]` |
@@ -36,6 +36,33 @@ Whatsapp::Utils::LanguageCodes.valid?("en_US") # => true
36
36
  Whatsapp::Utils::LanguageCodes.all.size # => 71
37
37
  ```
38
38
 
39
+ ### Authentication templates cannot go to a BSUID
40
+
41
+ One-tap, zero-tap, and copy-code
42
+ [authentication templates](../message_templates/authentication.md) require the
43
+ recipient's **phone number**. Address one to a
44
+ [BSUID](README.md#business-scoped-user-ids-bsuid) via `recipient:` and Meta answers
45
+ with error `131062`:
46
+
47
+ ```
48
+ Business-scoped User ID (BSUID) recipients are not supported for this message.
49
+ ```
50
+
51
+ The gem cannot catch this locally, and deliberately does not try. A template is
52
+ addressed at send time by `name` and `language` only — `Messages::Template` never
53
+ learns its own category, and the OTP configuration lives in the WABA. Guessing from
54
+ `components` would be worse than not guessing: a `copy_code` sub-type appears both on
55
+ copy-code authentication templates *and* on limited-time-offer marketing templates,
56
+ which have no such restriction, so any local rule would reject valid sends.
57
+
58
+ If you need to reach a username-only user with an authentication template, ask for
59
+ their phone number first — see the `REQUEST_CONTACT_INFO` button in
60
+ [components.md](../message_templates/components.md).
61
+
62
+ > Meta names one-tap, zero-tap, and copy-code specifically. Whether a plain
63
+ > `NO_BUTTONS` authentication template is also refused is **not documented** — treat it
64
+ > as untested rather than assuming either way.
65
+
39
66
  ## Components
40
67
 
41
68
  A component supplies the *values* for the placeholders the template defines. It has a
@@ -12,7 +12,7 @@ Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello from ruby-whatsap
12
12
 
13
13
  | Field | Required | Rules |
14
14
  | --- | --- | --- |
15
- | `to` | yes | The recipient's phone number |
15
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
16
16
  | `body` | yes | Max **4096** characters |
17
17
  | `preview_url` | no | `true` or `false`. **Defaults to `true`** |
18
18
 
@@ -10,7 +10,7 @@ Whatsapp::Messages.send_video!(to: "+15551234567", id: "1234567890", caption: "D
10
10
 
11
11
  | Field | Required | Rules |
12
12
  | --- | --- | --- |
13
- | `to` | yes | The recipient's phone number |
13
+ | `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
14
14
  | `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
15
15
  | `link` | one of | A publicly reachable HTTPS URL |
16
16
  | `caption` | no | Max **1024** characters |