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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +276 -634
  4. data/docs/README.md +73 -0
  5. data/docs/business_phone_number/README.md +232 -0
  6. data/docs/business_phone_number/account.md +170 -0
  7. data/docs/business_phone_number/profile.md +220 -0
  8. data/docs/configuration.md +172 -0
  9. data/docs/errors.md +208 -0
  10. data/docs/media/README.md +161 -0
  11. data/docs/message_templates/README.md +206 -0
  12. data/docs/message_templates/authentication.md +107 -0
  13. data/docs/message_templates/carousel.md +103 -0
  14. data/docs/message_templates/components.md +179 -0
  15. data/docs/message_templates/library.md +98 -0
  16. data/docs/message_templates/limited_time_offer.md +94 -0
  17. data/docs/message_templates/responses.md +173 -0
  18. data/docs/message_templates/standard.md +138 -0
  19. data/docs/messages/README.md +135 -0
  20. data/docs/messages/address.md +86 -0
  21. data/docs/messages/audio.md +43 -0
  22. data/docs/messages/contacts.md +129 -0
  23. data/docs/messages/document.md +49 -0
  24. data/docs/messages/image.md +59 -0
  25. data/docs/messages/interactive.md +281 -0
  26. data/docs/messages/location.md +51 -0
  27. data/docs/messages/location_request.md +59 -0
  28. data/docs/messages/mark_message_as_read.md +84 -0
  29. data/docs/messages/reaction.md +59 -0
  30. data/docs/messages/sticker.md +40 -0
  31. data/docs/messages/template.md +166 -0
  32. data/docs/messages/text.md +55 -0
  33. data/docs/messages/video.md +41 -0
  34. data/docs/subscribed_app/README.md +139 -0
  35. data/docs/webhooks/README.md +209 -0
  36. data/docs/webhooks/account_alerts.md +44 -0
  37. data/docs/webhooks/account_review_update.md +29 -0
  38. data/docs/webhooks/account_update.md +54 -0
  39. data/docs/webhooks/automatic_events.md +47 -0
  40. data/docs/webhooks/business_capability_update.md +40 -0
  41. data/docs/webhooks/history.md +44 -0
  42. data/docs/webhooks/message_template_components_update.md +46 -0
  43. data/docs/webhooks/message_template_quality_update.md +49 -0
  44. data/docs/webhooks/message_template_status_update.md +55 -0
  45. data/docs/webhooks/messages.md +385 -0
  46. data/docs/webhooks/partner_solutions.md +35 -0
  47. data/docs/webhooks/payment_configuration_update.md +40 -0
  48. data/docs/webhooks/phone_number_name_update.md +44 -0
  49. data/docs/webhooks/phone_number_quality_update.md +40 -0
  50. data/docs/webhooks/security.md +43 -0
  51. data/docs/webhooks/smb_app_state_sync.md +53 -0
  52. data/docs/webhooks/smb_message_echoes.md +54 -0
  53. data/docs/webhooks/template_category_update.md +52 -0
  54. data/docs/webhooks/user_preferences.md +48 -0
  55. data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
  56. data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
  57. data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
  58. data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
  59. data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
  60. data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
  61. data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
  62. data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
  63. data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
  64. data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
  65. data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
  66. data/lib/ruby/whatsapp/business_phone_number/transport.rb +11 -12
  67. data/lib/ruby/whatsapp/business_phone_number.rb +11 -4
  68. data/lib/ruby/whatsapp/message_templates.rb +2 -6
  69. data/lib/ruby/whatsapp/path_building.rb +33 -0
  70. data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
  71. data/lib/ruby/whatsapp/version.rb +1 -1
  72. 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
+ ```