ruby-whatsapp 0.4.0 → 0.4.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 +9 -0
- data/README.md +276 -634
- data/docs/README.md +73 -0
- data/docs/business_phone_number/README.md +232 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/configuration.md +172 -0
- data/docs/errors.md +208 -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 +179 -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 +135 -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 +281 -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 +166 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +209 -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/history.md +44 -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 +385 -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 +53 -0
- data/docs/webhooks/smb_message_echoes.md +54 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_preferences.md +48 -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/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 +11 -12
- data/lib/ruby/whatsapp/business_phone_number.rb +11 -4
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- metadata +63 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Contact Messages
|
|
2
|
+
|
|
3
|
+
Shares a structured, vCard-like contact card the recipient can tap once to save,
|
|
4
|
+
instead of pasting a phone number into a text message and hoping they copy it
|
|
5
|
+
correctly.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Whatsapp::Messages.send_contacts!(
|
|
9
|
+
to: "+15551234567",
|
|
10
|
+
contacts: [{ name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" } }]
|
|
11
|
+
)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Fields
|
|
15
|
+
|
|
16
|
+
| Field | Required | Rules |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `to` | yes | The recipient's phone number |
|
|
19
|
+
| `contacts` | yes | An array containing **exactly one** contact |
|
|
20
|
+
|
|
21
|
+
> **Exactly one contact.** The Cloud API currently accepts a single contact per
|
|
22
|
+
> message. An empty array raises `Contacts can't be blank`; two or more raises
|
|
23
|
+
> `Contacts is too long`. To share several people, send several messages.
|
|
24
|
+
|
|
25
|
+
Each entry in `contacts` is a hash (or a `Contacts::Contact`) composed of:
|
|
26
|
+
|
|
27
|
+
| Key | Required | Type |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `name` | yes | `Name` |
|
|
30
|
+
| `phones` | no | `Array<Phone>` |
|
|
31
|
+
| `emails` | no | `Array<Email>` |
|
|
32
|
+
| `addresses` | no | `Array<Address>` |
|
|
33
|
+
| `urls` | no | `Array<Url>` |
|
|
34
|
+
| `org` | no | `Org` |
|
|
35
|
+
| `birthday` | no | `"YYYY-MM-DD"` string |
|
|
36
|
+
|
|
37
|
+
### Sub-object fields
|
|
38
|
+
|
|
39
|
+
| Object | Fields | Notes |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `Name` | `formatted_name` (**required**), `first_name`, `last_name`, `middle_name`, `prefix`, `suffix` | |
|
|
42
|
+
| `Phone` | `phone` (**required**), `type`, `wa_id` | `type`: `CELL` `MAIN` `IPHONE` `HOME` `WORK` `OTHER` |
|
|
43
|
+
| `Email` | `email` (**required**), `type` | `type`: `HOME` `WORK` `OTHER` |
|
|
44
|
+
| `Address` | `street`, `city`, `state`, `zip`, `country`, `country_code`, `type` | all optional; `type`: `HOME` `WORK` `OTHER` |
|
|
45
|
+
| `Url` | `url` (**required**), `type` | `type`: `WEBSITE` `HOMEPAGE` `WORK` `HOME` `OTHER` |
|
|
46
|
+
| `Org` | `company`, `department`, `title` | all optional, no validation |
|
|
47
|
+
|
|
48
|
+
## A full example
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Whatsapp::Messages.send_contacts!(
|
|
52
|
+
to: "+15551234567",
|
|
53
|
+
contacts: [
|
|
54
|
+
{
|
|
55
|
+
name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" },
|
|
56
|
+
phones: [{ phone: "+15550001111", type: "WORK" }],
|
|
57
|
+
emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
58
|
+
addresses: [{ street: "1 Hacker Way", city: "Menlo Park", state: "CA",
|
|
59
|
+
zip: "94025", country: "United States", country_code: "us", type: "WORK" }],
|
|
60
|
+
urls: [{ url: "https://example.com", type: "WEBSITE" }],
|
|
61
|
+
org: { company: "Acme Inc.", department: "Support", title: "Lead" },
|
|
62
|
+
birthday: "1990-05-12",
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Serialized payload
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Whatsapp::Messages::Contacts.new(
|
|
72
|
+
to: "+15551234567",
|
|
73
|
+
contacts: [
|
|
74
|
+
{
|
|
75
|
+
name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" },
|
|
76
|
+
phones: [{ phone: "+15550001111", type: "WORK" }],
|
|
77
|
+
emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
78
|
+
org: { company: "Acme Inc." },
|
|
79
|
+
birthday: "1990-05-12",
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
).serialize
|
|
83
|
+
# => {
|
|
84
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "contacts",
|
|
85
|
+
# contacts: [{
|
|
86
|
+
# name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" },
|
|
87
|
+
# phones: [{ phone: "+15550001111", type: "WORK" }],
|
|
88
|
+
# emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
89
|
+
# org: { company: "Acme Inc." },
|
|
90
|
+
# birthday: "1990-05-12"
|
|
91
|
+
# }]
|
|
92
|
+
# }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Every sub-object compacts its own hash, and `Contact` omits `phones`/`emails`/
|
|
96
|
+
`addresses`/`urls` entirely when they're empty — so a minimal card is just `name`.
|
|
97
|
+
|
|
98
|
+
> **Known gap.** `Contacts::Name` validates only the presence of `formatted_name`, but
|
|
99
|
+
> live testing returns `(#131009) ContactName should have atleast one optional value be
|
|
100
|
+
> set along with formatted Name`. Meta requires at least one *additional* name field
|
|
101
|
+
> (e.g. `first_name`) alongside `formatted_name`; this class doesn't yet enforce it, so
|
|
102
|
+
> a name-only card passes local validation and is rejected server-side. Always set a
|
|
103
|
+
> second name field.
|
|
104
|
+
|
|
105
|
+
## Validation errors
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [])
|
|
109
|
+
# => ActiveModel::ValidationError: Contacts can't be blank
|
|
110
|
+
|
|
111
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [contact_a, contact_b])
|
|
112
|
+
# => ActiveModel::ValidationError: Contacts is too long (maximum is 1 character)
|
|
113
|
+
|
|
114
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [{ name: { first_name: "Jane" } }])
|
|
115
|
+
# => ArgumentError: missing keyword: :formatted_name
|
|
116
|
+
|
|
117
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [{ name: { formatted_name: "" } }])
|
|
118
|
+
# => ActiveModel::ValidationError: Formatted name can't be blank
|
|
119
|
+
|
|
120
|
+
Whatsapp::Messages.send_contacts!(
|
|
121
|
+
to: "+15551234567",
|
|
122
|
+
contacts: [{ name: { formatted_name: "Jane Doe" }, phones: [{ phone: "+1", type: "MOBILE" }] }]
|
|
123
|
+
)
|
|
124
|
+
# => ActiveModel::ValidationError: Type is not included in the list
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/contacts-messages>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Document Messages
|
|
2
|
+
|
|
3
|
+
Shares a file — an invoice, a contract, a PDF — with an optional caption and the
|
|
4
|
+
filename the recipient sees.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
Whatsapp::Messages.send_document!(
|
|
8
|
+
to: "+15551234567",
|
|
9
|
+
link: "https://example.com/invoice.pdf",
|
|
10
|
+
filename: "invoice.pdf"
|
|
11
|
+
)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Fields
|
|
15
|
+
|
|
16
|
+
| Field | Required | Rules |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `to` | yes | The recipient's phone number |
|
|
19
|
+
| `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
|
|
20
|
+
| `link` | one of | A publicly reachable HTTPS URL |
|
|
21
|
+
| `caption` | no | Max **1024** characters |
|
|
22
|
+
| `filename` | no | The name shown to the recipient; not validated |
|
|
23
|
+
|
|
24
|
+
**Either `id` or `link` is required.** Without `filename`, WhatsApp falls back to
|
|
25
|
+
whatever it can infer from the URL or media asset, which is usually an unhelpful
|
|
26
|
+
hash — set it whenever the recipient will save the file.
|
|
27
|
+
|
|
28
|
+
## Serialized payload
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
Whatsapp::Messages::Document.new(
|
|
32
|
+
to: "+15551234567", link: "https://example.com/invoice.pdf", filename: "invoice.pdf"
|
|
33
|
+
).serialize
|
|
34
|
+
# => {
|
|
35
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "document",
|
|
36
|
+
# document: { link: "https://example.com/invoice.pdf", filename: "invoice.pdf" }
|
|
37
|
+
# }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Validation errors
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
Whatsapp::Messages.send_document!(to: "+15551234567", filename: "invoice.pdf")
|
|
44
|
+
# => ActiveModel::ValidationError: Either id or link must be present
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/document-messages>
|
|
@@ -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` | yes | The recipient's phone number |
|
|
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,281 @@
|
|
|
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
|
+
|
|
18
|
+
## Shared fields
|
|
19
|
+
|
|
20
|
+
| Field | Required | Rules |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `to` | yes | The recipient's phone number |
|
|
23
|
+
| `type` | yes | One of the five keys above (Symbol or String) |
|
|
24
|
+
| `body` | yes | Body text, max **1024** characters |
|
|
25
|
+
| `action` | yes | Shape depends on `type` — see each section |
|
|
26
|
+
| `header` | no | `{ type: "text"\|"image"\|"video"\|"document", ... }` |
|
|
27
|
+
| `footer` | no | `{ text: "..." }`, max **60** characters |
|
|
28
|
+
|
|
29
|
+
### Header
|
|
30
|
+
|
|
31
|
+
A text header is capped at **60** characters. Media headers take a **`link:` only** —
|
|
32
|
+
there is no media-`id` path on interactive headers, unlike [image](image.md) and
|
|
33
|
+
friends:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
header: { type: "text", text: "Order #1234" }
|
|
37
|
+
header: { type: "image", link: "https://example.com/photo.jpg" }
|
|
38
|
+
header: { type: "video", link: "https://example.com/clip.mp4" }
|
|
39
|
+
header: { type: "document", link: "https://example.com/invoice.pdf" }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A header missing its content raises `Whatsapp::Messages::Interactive::Header::HeaderError`
|
|
43
|
+
(not a validation error):
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
Whatsapp::Messages.send_interactive!(
|
|
47
|
+
to: "+15551234567", type: :reply_buttons, body: "Pick one",
|
|
48
|
+
header: { type: "image" },
|
|
49
|
+
action: { buttons: [{ id: "a", title: "A" }] }
|
|
50
|
+
)
|
|
51
|
+
# => Whatsapp::Messages::Interactive::Header::HeaderError: Image link is required for image header
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Reply buttons
|
|
57
|
+
|
|
58
|
+
Up to **3** quick-reply buttons. The user's tap comes back as a webhook
|
|
59
|
+
`Message::Interactive` with `interactive_type == "button_reply"`.
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
Whatsapp::Messages.send_interactive!(
|
|
63
|
+
to: "+15551234567",
|
|
64
|
+
type: :reply_buttons,
|
|
65
|
+
body: "Would you like to confirm your order?",
|
|
66
|
+
action: { buttons: [{ id: "confirm", title: "Confirm" },
|
|
67
|
+
{ id: "cancel", title: "Cancel" }] }
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Field | Rules |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `buttons` | 1–**3** entries |
|
|
74
|
+
| `buttons[].id` | Required, max **256** characters — echoed back on tap |
|
|
75
|
+
| `buttons[].title` | Required, max **20** characters — the visible label |
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
Whatsapp::Messages::Interactive.new(
|
|
79
|
+
to: "+15551234567", type: :reply_buttons, body: "Would you like to confirm your order?",
|
|
80
|
+
action: { buttons: [{ id: "confirm", title: "Confirm" }, { id: "cancel", title: "Cancel" }] }
|
|
81
|
+
).serialize
|
|
82
|
+
# => { messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567",
|
|
83
|
+
# type: "interactive",
|
|
84
|
+
# interactive: { type: "button",
|
|
85
|
+
# body: { text: "Would you like to confirm your order?" },
|
|
86
|
+
# action: { buttons: [{ type: "reply", reply: { id: "confirm", title: "Confirm" } },
|
|
87
|
+
# { type: "reply", reply: { id: "cancel", title: "Cancel" } }] } } }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## List
|
|
93
|
+
|
|
94
|
+
One button that expands into up to **10 sections** of up to **10 rows** each. Use it
|
|
95
|
+
when three buttons aren't enough — a menu, a time-slot picker, a product list.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
Whatsapp::Messages.send_interactive!(
|
|
99
|
+
to: "+15551234567",
|
|
100
|
+
type: :list_buttons,
|
|
101
|
+
body: "Choose a drink",
|
|
102
|
+
action: {
|
|
103
|
+
button: "Menu",
|
|
104
|
+
sections: [
|
|
105
|
+
{ title: "Coffee", rows: [
|
|
106
|
+
{ id: "espresso", title: "Espresso", description: "Strong & short" },
|
|
107
|
+
{ id: "latte", title: "Latte", description: "Milky & mild" },
|
|
108
|
+
] },
|
|
109
|
+
],
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Field | Rules |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `button` | Required, max **20** characters — the label that opens the list |
|
|
117
|
+
| `sections` | **1–10** sections |
|
|
118
|
+
| `sections[].title` | Required, max **24** characters |
|
|
119
|
+
| `sections[].rows` | **1–10** rows |
|
|
120
|
+
| `rows[].id` | Required, max **200** characters |
|
|
121
|
+
| `rows[].title` | Required, max **24** characters |
|
|
122
|
+
| `rows[].description` | Optional, max **72** characters |
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
Whatsapp::Messages::Interactive.new(
|
|
126
|
+
to: "+15551234567", type: :list_buttons, body: "Choose a drink",
|
|
127
|
+
action: { button: "Menu", sections: [
|
|
128
|
+
{ title: "Coffee", rows: [{ id: "espresso", title: "Espresso", description: "Strong & short" }] },
|
|
129
|
+
] }
|
|
130
|
+
).serialize
|
|
131
|
+
# => { ..., interactive: { type: "list", body: { text: "Choose a drink" },
|
|
132
|
+
# action: { button: "Menu",
|
|
133
|
+
# sections: [{ title: "Coffee",
|
|
134
|
+
# rows: [{ id: "espresso", title: "Espresso",
|
|
135
|
+
# description: "Strong & short" }] }] } } }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A tap arrives back as a webhook `Message::Interactive` with
|
|
139
|
+
`interactive_type == "list_reply"`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## CTA URL button
|
|
144
|
+
|
|
145
|
+
Surfaces a link as a tappable button instead of raw text in the body — clearer, and it
|
|
146
|
+
sidesteps the link preview.
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Whatsapp::Messages.send_interactive!(
|
|
150
|
+
to: "+15551234567",
|
|
151
|
+
type: :url_button,
|
|
152
|
+
body: "Check out our new arrivals",
|
|
153
|
+
action: { name: "cta_url", display_text: "Shop now", url: "https://example.com/new" }
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Field | Rules |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `name` | Required, max **20** characters — always `"cta_url"` |
|
|
160
|
+
| `display_text` | Required, max **20** characters — the button label |
|
|
161
|
+
| `url` | The destination. **Not validated**, not even for presence |
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
Whatsapp::Messages::Interactive.new(
|
|
165
|
+
to: "+15551234567", type: :url_button, body: "Check out our new arrivals",
|
|
166
|
+
action: { name: "cta_url", display_text: "Shop now", url: "https://example.com/new" }
|
|
167
|
+
).serialize
|
|
168
|
+
# => { ..., interactive: { type: "cta_url", body: { text: "Check out our new arrivals" },
|
|
169
|
+
# action: { name: "cta_url",
|
|
170
|
+
# parameters: { display_text: "Shop now", url: "https://example.com/new" } } } }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Media carousel
|
|
176
|
+
|
|
177
|
+
**2–10** swipeable cards, each with its own header, body, CTA URL, and quick-reply
|
|
178
|
+
buttons. `card_index` is assigned automatically from array position — do not pass it.
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
Whatsapp::Messages.send_interactive!(
|
|
182
|
+
to: "+15551234567",
|
|
183
|
+
type: :media_carousel,
|
|
184
|
+
body: "Today's picks",
|
|
185
|
+
action: {
|
|
186
|
+
cards: [
|
|
187
|
+
{
|
|
188
|
+
header: { type: "image", link: "https://example.com/1.jpg" },
|
|
189
|
+
body: "Item one",
|
|
190
|
+
action: { name: "cta_url", display_text: "Buy", url: "https://example.com/1" },
|
|
191
|
+
buttons: [{ quick_reply: { id: "q1", title: "Details" } }],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
header: { type: "image", link: "https://example.com/2.jpg" },
|
|
195
|
+
body: "Item two",
|
|
196
|
+
action: { name: "cta_url", display_text: "Buy", url: "https://example.com/2" },
|
|
197
|
+
buttons: [{ quick_reply: { id: "q2", title: "Details" } }],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| Field | Rules |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| `cards` | **2–10** cards |
|
|
207
|
+
| `cards[].header` | Required — same shape as the message header (`link:` only for media) |
|
|
208
|
+
| `cards[].body` | Required |
|
|
209
|
+
| `cards[].action` | Required — a CTA URL action |
|
|
210
|
+
| `cards[].buttons` | Required — array of `{ quick_reply: { id:, title: } }` |
|
|
211
|
+
|
|
212
|
+
All five card keys are required Ruby keywords; omitting one raises `ArgumentError`,
|
|
213
|
+
not a validation error.
|
|
214
|
+
|
|
215
|
+
> **Two caveats.** The wire value `"carousel"` is flagged in the source as worth
|
|
216
|
+
> re-checking against Meta's docs for your API version before relying on it in
|
|
217
|
+
> production. And `MediaCarousel::Card::Button::QuickReply` declares `id`/`title`
|
|
218
|
+
> length validations but never runs `validate!`, so an over-long or blank
|
|
219
|
+
> `id`/`title` serializes silently and is rejected by Meta instead of locally.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Product carousel
|
|
224
|
+
|
|
225
|
+
**2–10** cards referencing products in your Meta catalog. Like the media carousel,
|
|
226
|
+
`card_index` is assigned from array position.
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
Whatsapp::Messages.send_interactive!(
|
|
230
|
+
to: "+15551234567",
|
|
231
|
+
type: :product_carousel,
|
|
232
|
+
body: "Recommended for you",
|
|
233
|
+
action: {
|
|
234
|
+
cards: [
|
|
235
|
+
{ catalog_id: "123456789", product_retailer_id: "SKU-1" },
|
|
236
|
+
{ catalog_id: "123456789", product_retailer_id: "SKU-2" },
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Field | Rules |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| `cards` | **2–10** cards |
|
|
245
|
+
| `cards[].catalog_id` | Required (Ruby keyword) |
|
|
246
|
+
| `cards[].product_retailer_id` | Required (Ruby keyword) |
|
|
247
|
+
|
|
248
|
+
Card contents carry **no validations at all** — a wrong catalog or SKU surfaces
|
|
249
|
+
server-side.
|
|
250
|
+
|
|
251
|
+
> The wire value `"product_list"` carries the same "verify against your API version"
|
|
252
|
+
> caveat as the media carousel.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Validation errors
|
|
257
|
+
|
|
258
|
+
`Interactive` itself validates `type`, `body`, and `action` presence at construction.
|
|
259
|
+
The **nested** action rules — button counts, title lengths, section limits — run when
|
|
260
|
+
the payload is serialized, which for `send_interactive!` is still before any HTTP
|
|
261
|
+
request:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
Whatsapp::Messages.send_interactive!(
|
|
265
|
+
to: "+15551234567", type: :telepathy, body: "Pick",
|
|
266
|
+
action: { buttons: [{ id: "a", title: "A" }] }
|
|
267
|
+
)
|
|
268
|
+
# => ActiveModel::ValidationError: Type is not included in the list
|
|
269
|
+
|
|
270
|
+
Whatsapp::Messages.send_interactive!(
|
|
271
|
+
to: "+15551234567", type: :reply_buttons, body: "Pick",
|
|
272
|
+
action: { buttons: [{ id: "a", title: "A really quite long button label" }] }
|
|
273
|
+
)
|
|
274
|
+
# => ActiveModel::ValidationError: Title is too long (maximum is 20 characters)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
**Meta docs:**
|
|
280
|
+
[CTA URL](https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/interactive-cta-url-messages)
|
|
281
|
+
· [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` | yes | The recipient's phone number |
|
|
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` | yes | The recipient's phone number |
|
|
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
|
+
```
|