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,59 @@
|
|
|
1
|
+
# Image Messages
|
|
2
|
+
|
|
3
|
+
Shares a photo or graphic — a product shot, a receipt, a screenshot.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Whatsapp::Messages.send_image!(
|
|
7
|
+
to: "+15551234567",
|
|
8
|
+
link: "https://example.com/photo.jpg",
|
|
9
|
+
caption: "Our new product"
|
|
10
|
+
)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Fields
|
|
14
|
+
|
|
15
|
+
| Field | Required | Rules |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
18
|
+
| `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
|
|
19
|
+
| `link` | one of | A publicly reachable HTTPS URL |
|
|
20
|
+
| `caption` | no | Max **1024** characters |
|
|
21
|
+
|
|
22
|
+
**Either `id` or `link` is required** — never both are mandatory, but at least one
|
|
23
|
+
must be present. Prefer `id` when you'll send the same asset more than once: Meta
|
|
24
|
+
caches an uploaded asset, whereas a `link` is re-fetched on every send.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
media = Whatsapp::Media.new
|
|
28
|
+
media_id = media.upload(file_path: "photo.jpg", type: "image/jpeg")
|
|
29
|
+
|
|
30
|
+
Whatsapp::Messages.send_image!(to: "+15551234567", id: media_id, caption: "Our new product")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Serialized payload
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Whatsapp::Messages::Image.new(
|
|
37
|
+
to: "+15551234567", link: "https://example.com/photo.jpg", caption: "Our new product"
|
|
38
|
+
).serialize
|
|
39
|
+
# => {
|
|
40
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "image",
|
|
41
|
+
# image: { link: "https://example.com/photo.jpg", caption: "Our new product" }
|
|
42
|
+
# }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `image` hash is compacted, so absent optional keys never reach the wire.
|
|
46
|
+
|
|
47
|
+
## Validation errors
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
Whatsapp::Messages.send_image!(to: "+15551234567")
|
|
51
|
+
# => ActiveModel::ValidationError: Either id or link must be present
|
|
52
|
+
|
|
53
|
+
Whatsapp::Messages.send_image!(to: "+15551234567", link: "https://example.com/p.jpg", caption: "x" * 1025)
|
|
54
|
+
# => ActiveModel::ValidationError: Caption is too long (maximum is 1024 characters)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/image-messages>
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Interactive Messages
|
|
2
|
+
|
|
3
|
+
Gives the user tappable UI — buttons, a list, a carousel — instead of asking them to
|
|
4
|
+
type a free-text reply. Tapping "Confirm" is one action; typing "yes", "Yes", "yeah",
|
|
5
|
+
or "ya" is four things you have to parse.
|
|
6
|
+
|
|
7
|
+
Unlike every other kind, `Interactive` is a **single class** that dispatches on
|
|
8
|
+
`type:` through a frozen registry:
|
|
9
|
+
|
|
10
|
+
| `type:` | Wire `interactive.type` | Shape |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| `:reply_buttons` | `"button"` | [Up to 3 quick-reply buttons](#reply-buttons) |
|
|
13
|
+
| `:list_buttons` | `"list"` | [A button expanding into sections of rows](#list) |
|
|
14
|
+
| `:url_button` | `"cta_url"` | [A link rendered as a button](#cta-url-button) |
|
|
15
|
+
| `:media_carousel` | `"carousel"` | [2–10 swipeable media cards](#media-carousel) |
|
|
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) |
|
|
18
|
+
|
|
19
|
+
## Shared fields
|
|
20
|
+
|
|
21
|
+
| Field | Required | Rules |
|
|
22
|
+
| --- | --- | --- |
|
|
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) |
|
|
25
|
+
| `body` | yes | Body text, max **1024** characters |
|
|
26
|
+
| `action` | usually | Shape depends on `type` — see each section. Omit it only for `:request_contact_info`, the one kind with nothing to supply |
|
|
27
|
+
| `header` | no | `{ type: "text"\|"image"\|"video"\|"document", ... }` |
|
|
28
|
+
| `footer` | no | `{ text: "..." }`, max **60** characters |
|
|
29
|
+
|
|
30
|
+
### Header
|
|
31
|
+
|
|
32
|
+
A text header is capped at **60** characters. Media headers take a **`link:` only** —
|
|
33
|
+
there is no media-`id` path on interactive headers, unlike [image](image.md) and
|
|
34
|
+
friends:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
header: { type: "text", text: "Order #1234" }
|
|
38
|
+
header: { type: "image", link: "https://example.com/photo.jpg" }
|
|
39
|
+
header: { type: "video", link: "https://example.com/clip.mp4" }
|
|
40
|
+
header: { type: "document", link: "https://example.com/invoice.pdf" }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A header missing its content raises `Whatsapp::Messages::Interactive::Header::HeaderError`
|
|
44
|
+
(not a validation error):
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
Whatsapp::Messages.send_interactive!(
|
|
48
|
+
to: "+15551234567", type: :reply_buttons, body: "Pick one",
|
|
49
|
+
header: { type: "image" },
|
|
50
|
+
action: { buttons: [{ id: "a", title: "A" }] }
|
|
51
|
+
)
|
|
52
|
+
# => Whatsapp::Messages::Interactive::Header::HeaderError: Image link is required for image header
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Reply buttons
|
|
58
|
+
|
|
59
|
+
Up to **3** quick-reply buttons. The user's tap comes back as a webhook
|
|
60
|
+
`Message::Interactive` with `interactive_type == "button_reply"`.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Whatsapp::Messages.send_interactive!(
|
|
64
|
+
to: "+15551234567",
|
|
65
|
+
type: :reply_buttons,
|
|
66
|
+
body: "Would you like to confirm your order?",
|
|
67
|
+
action: { buttons: [{ id: "confirm", title: "Confirm" },
|
|
68
|
+
{ id: "cancel", title: "Cancel" }] }
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Field | Rules |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `buttons` | 1–**3** entries |
|
|
75
|
+
| `buttons[].id` | Required, max **256** characters — echoed back on tap |
|
|
76
|
+
| `buttons[].title` | Required, max **20** characters — the visible label |
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
Whatsapp::Messages::Interactive.new(
|
|
80
|
+
to: "+15551234567", type: :reply_buttons, body: "Would you like to confirm your order?",
|
|
81
|
+
action: { buttons: [{ id: "confirm", title: "Confirm" }, { id: "cancel", title: "Cancel" }] }
|
|
82
|
+
).serialize
|
|
83
|
+
# => { messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567",
|
|
84
|
+
# type: "interactive",
|
|
85
|
+
# interactive: { type: "button",
|
|
86
|
+
# body: { text: "Would you like to confirm your order?" },
|
|
87
|
+
# action: { buttons: [{ type: "reply", reply: { id: "confirm", title: "Confirm" } },
|
|
88
|
+
# { type: "reply", reply: { id: "cancel", title: "Cancel" } }] } } }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## List
|
|
94
|
+
|
|
95
|
+
One button that expands into up to **10 sections** of up to **10 rows** each. Use it
|
|
96
|
+
when three buttons aren't enough — a menu, a time-slot picker, a product list.
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
Whatsapp::Messages.send_interactive!(
|
|
100
|
+
to: "+15551234567",
|
|
101
|
+
type: :list_buttons,
|
|
102
|
+
body: "Choose a drink",
|
|
103
|
+
action: {
|
|
104
|
+
button: "Menu",
|
|
105
|
+
sections: [
|
|
106
|
+
{ title: "Coffee", rows: [
|
|
107
|
+
{ id: "espresso", title: "Espresso", description: "Strong & short" },
|
|
108
|
+
{ id: "latte", title: "Latte", description: "Milky & mild" },
|
|
109
|
+
] },
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Field | Rules |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| `button` | Required, max **20** characters — the label that opens the list |
|
|
118
|
+
| `sections` | **1–10** sections |
|
|
119
|
+
| `sections[].title` | Required, max **24** characters |
|
|
120
|
+
| `sections[].rows` | **1–10** rows |
|
|
121
|
+
| `rows[].id` | Required, max **200** characters |
|
|
122
|
+
| `rows[].title` | Required, max **24** characters |
|
|
123
|
+
| `rows[].description` | Optional, max **72** characters |
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Whatsapp::Messages::Interactive.new(
|
|
127
|
+
to: "+15551234567", type: :list_buttons, body: "Choose a drink",
|
|
128
|
+
action: { button: "Menu", sections: [
|
|
129
|
+
{ title: "Coffee", rows: [{ id: "espresso", title: "Espresso", description: "Strong & short" }] },
|
|
130
|
+
] }
|
|
131
|
+
).serialize
|
|
132
|
+
# => { ..., interactive: { type: "list", body: { text: "Choose a drink" },
|
|
133
|
+
# action: { button: "Menu",
|
|
134
|
+
# sections: [{ title: "Coffee",
|
|
135
|
+
# rows: [{ id: "espresso", title: "Espresso",
|
|
136
|
+
# description: "Strong & short" }] }] } } }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A tap arrives back as a webhook `Message::Interactive` with
|
|
140
|
+
`interactive_type == "list_reply"`.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## CTA URL button
|
|
145
|
+
|
|
146
|
+
Surfaces a link as a tappable button instead of raw text in the body — clearer, and it
|
|
147
|
+
sidesteps the link preview.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
Whatsapp::Messages.send_interactive!(
|
|
151
|
+
to: "+15551234567",
|
|
152
|
+
type: :url_button,
|
|
153
|
+
body: "Check out our new arrivals",
|
|
154
|
+
action: { name: "cta_url", display_text: "Shop now", url: "https://example.com/new" }
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Field | Rules |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `name` | Required, max **20** characters — always `"cta_url"` |
|
|
161
|
+
| `display_text` | Required, max **20** characters — the button label |
|
|
162
|
+
| `url` | The destination. **Not validated**, not even for presence |
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
Whatsapp::Messages::Interactive.new(
|
|
166
|
+
to: "+15551234567", type: :url_button, body: "Check out our new arrivals",
|
|
167
|
+
action: { name: "cta_url", display_text: "Shop now", url: "https://example.com/new" }
|
|
168
|
+
).serialize
|
|
169
|
+
# => { ..., interactive: { type: "cta_url", body: { text: "Check out our new arrivals" },
|
|
170
|
+
# action: { name: "cta_url",
|
|
171
|
+
# parameters: { display_text: "Shop now", url: "https://example.com/new" } } } }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Media carousel
|
|
177
|
+
|
|
178
|
+
**2–10** swipeable cards, each with its own header, body, CTA URL, and quick-reply
|
|
179
|
+
buttons. `card_index` is assigned automatically from array position — do not pass it.
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
Whatsapp::Messages.send_interactive!(
|
|
183
|
+
to: "+15551234567",
|
|
184
|
+
type: :media_carousel,
|
|
185
|
+
body: "Today's picks",
|
|
186
|
+
action: {
|
|
187
|
+
cards: [
|
|
188
|
+
{
|
|
189
|
+
header: { type: "image", link: "https://example.com/1.jpg" },
|
|
190
|
+
body: "Item one",
|
|
191
|
+
action: { name: "cta_url", display_text: "Buy", url: "https://example.com/1" },
|
|
192
|
+
buttons: [{ quick_reply: { id: "q1", title: "Details" } }],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
header: { type: "image", link: "https://example.com/2.jpg" },
|
|
196
|
+
body: "Item two",
|
|
197
|
+
action: { name: "cta_url", display_text: "Buy", url: "https://example.com/2" },
|
|
198
|
+
buttons: [{ quick_reply: { id: "q2", title: "Details" } }],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| Field | Rules |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `cards` | **2–10** cards |
|
|
208
|
+
| `cards[].header` | Required — same shape as the message header (`link:` only for media) |
|
|
209
|
+
| `cards[].body` | Required |
|
|
210
|
+
| `cards[].action` | Required — a CTA URL action |
|
|
211
|
+
| `cards[].buttons` | Required — array of `{ quick_reply: { id:, title: } }` |
|
|
212
|
+
|
|
213
|
+
All five card keys are required Ruby keywords; omitting one raises `ArgumentError`,
|
|
214
|
+
not a validation error.
|
|
215
|
+
|
|
216
|
+
> **Two caveats.** The wire value `"carousel"` is flagged in the source as worth
|
|
217
|
+
> re-checking against Meta's docs for your API version before relying on it in
|
|
218
|
+
> production. And `MediaCarousel::Card::Button::QuickReply` declares `id`/`title`
|
|
219
|
+
> length validations but never runs `validate!`, so an over-long or blank
|
|
220
|
+
> `id`/`title` serializes silently and is rejected by Meta instead of locally.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Product carousel
|
|
225
|
+
|
|
226
|
+
**2–10** cards referencing products in your Meta catalog. Like the media carousel,
|
|
227
|
+
`card_index` is assigned from array position.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
Whatsapp::Messages.send_interactive!(
|
|
231
|
+
to: "+15551234567",
|
|
232
|
+
type: :product_carousel,
|
|
233
|
+
body: "Recommended for you",
|
|
234
|
+
action: {
|
|
235
|
+
cards: [
|
|
236
|
+
{ catalog_id: "123456789", product_retailer_id: "SKU-1" },
|
|
237
|
+
{ catalog_id: "123456789", product_retailer_id: "SKU-2" },
|
|
238
|
+
],
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| Field | Rules |
|
|
244
|
+
| --- | --- |
|
|
245
|
+
| `cards` | **2–10** cards |
|
|
246
|
+
| `cards[].catalog_id` | Required (Ruby keyword) |
|
|
247
|
+
| `cards[].product_retailer_id` | Required (Ruby keyword) |
|
|
248
|
+
|
|
249
|
+
Card contents carry **no validations at all** — a wrong catalog or SKU surfaces
|
|
250
|
+
server-side.
|
|
251
|
+
|
|
252
|
+
> The wire value `"product_list"` carries the same "verify against your API version"
|
|
253
|
+
> caveat as the media carousel.
|
|
254
|
+
|
|
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
|
+
|
|
317
|
+
## Validation errors
|
|
318
|
+
|
|
319
|
+
`Interactive` itself validates `type`, `body`, and `action` presence at construction.
|
|
320
|
+
The **nested** action rules — button counts, title lengths, section limits — run when
|
|
321
|
+
the payload is serialized, which for `send_interactive!` is still before any HTTP
|
|
322
|
+
request:
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
Whatsapp::Messages.send_interactive!(
|
|
326
|
+
to: "+15551234567", type: :telepathy, body: "Pick",
|
|
327
|
+
action: { buttons: [{ id: "a", title: "A" }] }
|
|
328
|
+
)
|
|
329
|
+
# => ActiveModel::ValidationError: Type is not included in the list
|
|
330
|
+
|
|
331
|
+
Whatsapp::Messages.send_interactive!(
|
|
332
|
+
to: "+15551234567", type: :reply_buttons, body: "Pick",
|
|
333
|
+
action: { buttons: [{ id: "a", title: "A really quite long button label" }] }
|
|
334
|
+
)
|
|
335
|
+
# => ActiveModel::ValidationError: Title is too long (maximum is 20 characters)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
**Meta docs:**
|
|
341
|
+
[CTA URL](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/interactive-cta-url-messages)
|
|
342
|
+
· [lists](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/interactive-list-messages)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Location Messages
|
|
2
|
+
|
|
3
|
+
Shares a fixed latitude/longitude pin — a shop, a pickup point, a venue. The inverse
|
|
4
|
+
direction (asking the *user* where they are) is
|
|
5
|
+
[Location Request](location_request.md).
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Whatsapp::Messages.send_location!(
|
|
9
|
+
to: "+15551234567",
|
|
10
|
+
latitude: 37.4847,
|
|
11
|
+
longitude: -122.1477,
|
|
12
|
+
name: "Meta HQ"
|
|
13
|
+
)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Fields
|
|
17
|
+
|
|
18
|
+
| Field | Required | Rules |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
21
|
+
| `latitude` | yes | Numeric |
|
|
22
|
+
| `longitude` | yes | Numeric |
|
|
23
|
+
| `name` | no | Label shown above the pin |
|
|
24
|
+
| `address` | no | Street address shown under the name |
|
|
25
|
+
|
|
26
|
+
Both coordinates are validated with `numericality`, so a string that doesn't parse as
|
|
27
|
+
a number is rejected locally. `name` and `address` are free text — WhatsApp renders
|
|
28
|
+
them as the pin's caption; without them the recipient sees bare coordinates.
|
|
29
|
+
|
|
30
|
+
## Serialized payload
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Whatsapp::Messages::Location.new(
|
|
34
|
+
to: "+15551234567", latitude: 37.4847, longitude: -122.1477, name: "Meta HQ"
|
|
35
|
+
).serialize
|
|
36
|
+
# => {
|
|
37
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "location",
|
|
38
|
+
# location: { latitude: 37.4847, longitude: -122.1477, name: "Meta HQ" }
|
|
39
|
+
# }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Validation errors
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Whatsapp::Messages.send_location!(to: "+15551234567", latitude: "here", longitude: -122.1477)
|
|
46
|
+
# => ActiveModel::ValidationError: Latitude is not a number
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/location-messages>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Location Request Messages
|
|
2
|
+
|
|
3
|
+
Asks the user to **share their current location back to the business** — the inverse
|
|
4
|
+
of [Location](location.md). Useful for coordinating a delivery or a pickup without
|
|
5
|
+
making the customer type an address.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Whatsapp::Messages.send_location_request!(
|
|
9
|
+
to: "+15551234567",
|
|
10
|
+
body: "Can you share your delivery location?"
|
|
11
|
+
)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Fields
|
|
15
|
+
|
|
16
|
+
| Field | Required | Rules |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
19
|
+
| `body` | yes | The prompt text, max **1024** characters |
|
|
20
|
+
|
|
21
|
+
## Serialized payload
|
|
22
|
+
|
|
23
|
+
Despite being its own Ruby class, this travels as an interactive message on the wire
|
|
24
|
+
(`type: "interactive"`, `interactive.type: "location_request_message"`):
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
Whatsapp::Messages::LocationRequest.new(
|
|
28
|
+
to: "+15551234567", body: "Can you share your delivery location?"
|
|
29
|
+
).serialize
|
|
30
|
+
# => {
|
|
31
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567",
|
|
32
|
+
# type: "interactive",
|
|
33
|
+
# interactive: {
|
|
34
|
+
# type: "location_request_message",
|
|
35
|
+
# body: "Can you share your delivery location?",
|
|
36
|
+
# action: { name: "send_location" }
|
|
37
|
+
# }
|
|
38
|
+
# }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> **Known bug — confirmed broken against the live API.** `body` is serialized as a
|
|
42
|
+
> bare String, but Meta's JSON schema requires `interactive.body` to be an object
|
|
43
|
+
> (`{ text: ... }`) or null. [`Address`](address.md) and
|
|
44
|
+
> [`Interactive`](interactive.md) both wrap body text correctly; this class does not,
|
|
45
|
+
> and live testing returns a schema rejection. Not yet fixed — track it before
|
|
46
|
+
> relying on this kind in production.
|
|
47
|
+
|
|
48
|
+
## The reply
|
|
49
|
+
|
|
50
|
+
The user's shared location arrives as an inbound webhook notification, deserialized
|
|
51
|
+
into `Whatsapp::Webhook::Message::Location` with `latitude`, `longitude`, `name`, and
|
|
52
|
+
`address`. See [../webhooks/messages.md](../webhooks/messages.md).
|
|
53
|
+
|
|
54
|
+
## Validation errors
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Whatsapp::Messages.send_location_request!(to: "+15551234567", body: "")
|
|
58
|
+
# => ActiveModel::ValidationError: Body can't be blank
|
|
59
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Mark Message As Read
|
|
2
|
+
|
|
3
|
+
Closes the read-receipt loop — this is what turns on the blue "seen" checkmarks on the
|
|
4
|
+
customer's side. Marking one message read also marks **every earlier message in that
|
|
5
|
+
conversation** as read.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Whatsapp::Messages.mark_message_as_read!(
|
|
9
|
+
message_id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJDQjZCMzlEQUE4OTJBMTE4RTUA"
|
|
10
|
+
)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Fields
|
|
14
|
+
|
|
15
|
+
| Field | Required | Rules |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `message_id` | yes | The WAMID of an inbound message |
|
|
18
|
+
| `client` | no | Defaults to a new `Whatsapp::Client` |
|
|
19
|
+
|
|
20
|
+
`message_id` is checked for presence only — there is no WAMID format check, so an
|
|
21
|
+
invalid ID surfaces server-side as error `131009`.
|
|
22
|
+
|
|
23
|
+
> **30-day window.** Meta requires the call within 30 days of receiving the message.
|
|
24
|
+
> Past that, the request fails.
|
|
25
|
+
|
|
26
|
+
## Why it isn't a `send_<kind>!` method
|
|
27
|
+
|
|
28
|
+
Every other kind shares the `to` / `recipient_type` / `type` envelope from
|
|
29
|
+
`Messages::Base`. This endpoint's payload is flat — no recipient, no type — so
|
|
30
|
+
`MarkMessageAsRead` doesn't inherit `Base` at all and isn't registered in
|
|
31
|
+
`Messages::KINDS`. Rather than carve an exception into the loop that generates
|
|
32
|
+
`send_<kind>!` methods, it gets its own hand-written class method.
|
|
33
|
+
|
|
34
|
+
## Serialized payload
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Whatsapp::Messages::MarkMessageAsRead.new(message_id: "wamid.HBg...").serialize
|
|
38
|
+
# => { messaging_product: "whatsapp", status: "read", message_id: "wamid.HBg..." }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The response
|
|
42
|
+
|
|
43
|
+
This endpoint replies `{"success": true}` rather than the usual
|
|
44
|
+
`{messaging_product, contacts, messages}`, so the shared `Messages::Response` carries
|
|
45
|
+
it on `#success`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
result = Whatsapp::Messages.mark_message_as_read!(message_id: "wamid.HBg...")
|
|
49
|
+
|
|
50
|
+
result.success # => true
|
|
51
|
+
result.messages # => [] — nothing to report for this endpoint
|
|
52
|
+
result.contacts # => []
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Errors
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
Whatsapp::Messages::MarkMessageAsRead.new(message_id: "")
|
|
59
|
+
# => ActiveModel::ValidationError: Message can't be blank
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A non-2xx response raises `Whatsapp::RequestError: Failed to mark message as read: ...`.
|
|
63
|
+
|
|
64
|
+
## Typical use
|
|
65
|
+
|
|
66
|
+
Call it from your webhook handler as soon as an inbound message is deserialized:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
notification.entry.each do |entry|
|
|
70
|
+
entry.changes.each do |change|
|
|
71
|
+
next unless change.field == "messages"
|
|
72
|
+
|
|
73
|
+
change.value.messages.each do |message|
|
|
74
|
+
Whatsapp::Messages.mark_message_as_read!(message_id: message.id)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See [../webhooks/messages.md](../webhooks/messages.md) for the inbound side.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/mark-message-as-read>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Reaction Messages
|
|
2
|
+
|
|
3
|
+
Acknowledges a previous message with an emoji, mirroring WhatsApp's native
|
|
4
|
+
tap-and-hold reaction, without cluttering the thread with a new text message.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: "👍")
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Fields
|
|
11
|
+
|
|
12
|
+
| Field | Required | Rules |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
15
|
+
| `message_id` | yes | The WAMID of the message being reacted to |
|
|
16
|
+
| `emoji` | yes | A single emoji, or `""` to remove |
|
|
17
|
+
|
|
18
|
+
`emoji` is checked against Ruby's `\p{Emoji}` property, so a plain word is rejected
|
|
19
|
+
locally rather than by Meta. `nil` is invalid; the empty string is not.
|
|
20
|
+
|
|
21
|
+
## Removing a reaction
|
|
22
|
+
|
|
23
|
+
Sending an empty `emoji` removes a reaction you previously sent to that message:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: "")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Serialized payload
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
Whatsapp::Messages::Reaction.new(
|
|
33
|
+
to: "+15551234567", message_id: "wamid.HBg...", emoji: "\u{1F44D}"
|
|
34
|
+
).serialize
|
|
35
|
+
# => {
|
|
36
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "reaction",
|
|
37
|
+
# reaction: { message_id: "wamid.HBg...", emoji: "👍" }
|
|
38
|
+
# }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `reaction` hash is not compacted — both keys always ship, which is what makes the
|
|
42
|
+
empty-string removal work.
|
|
43
|
+
|
|
44
|
+
## Validation errors
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: "nope")
|
|
48
|
+
# => ActiveModel::ValidationError: Emoji must be a valid emoji character
|
|
49
|
+
|
|
50
|
+
Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: nil)
|
|
51
|
+
# => ActiveModel::ValidationError: Emoji can't be nil
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`message_id` is checked for presence only — an existent-looking but wrong WAMID
|
|
55
|
+
surfaces server-side as error `131009`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/reaction-messages>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Sticker Messages
|
|
2
|
+
|
|
3
|
+
Sends a WhatsApp sticker asset (a WebP image, animated or static).
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Whatsapp::Messages.send_sticker!(to: "+15551234567", id: "STICKER_MEDIA_ID")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Fields
|
|
10
|
+
|
|
11
|
+
| Field | Required | Rules |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
14
|
+
| `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
|
|
15
|
+
| `link` | one of | A publicly reachable HTTPS URL |
|
|
16
|
+
|
|
17
|
+
**Either `id` or `link` is required** — prefer `id`. Stickers are almost always
|
|
18
|
+
reused across many conversations, so uploading once and holding the ID avoids
|
|
19
|
+
re-fetching the same asset on every send.
|
|
20
|
+
|
|
21
|
+
## Serialized payload
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
Whatsapp::Messages::Sticker.new(to: "+15551234567", id: "STICKER_MEDIA_ID").serialize
|
|
25
|
+
# => {
|
|
26
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "sticker",
|
|
27
|
+
# sticker: { id: "STICKER_MEDIA_ID" }
|
|
28
|
+
# }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Validation errors
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Whatsapp::Messages.send_sticker!(to: "+15551234567")
|
|
35
|
+
# => ActiveModel::ValidationError: Either id or link must be present
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/sticker-messages>
|