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,94 @@
1
+ # Limited-Time Offer Templates
2
+
3
+ Adds a live countdown and a copyable coupon code to a marketing message. The countdown
4
+ is rendered by WhatsApp itself, so the urgency stays accurate no matter when the
5
+ recipient opens the thread.
6
+
7
+ > **MARKETING only**, and the presence of a `LIMITED_TIME_OFFER` component tightens
8
+ > four other rules at once. See [The knock-on rules](#the-knock-on-rules).
9
+
10
+ ```ruby
11
+ Whatsapp::MessageTemplates.new.create(
12
+ name: "spring_offer", language: "en_US", category: "MARKETING",
13
+ components: [
14
+ { type: :header, format: "IMAGE", header_handle: "4::aW..." },
15
+ { type: :limited_time_offer, text: "Expiring offer!", has_expiration: true },
16
+ { type: :body, text: "Good news, {{1}}! Use code {{2}} for 25% off.",
17
+ example: ["Pablo", "SPRING25"] },
18
+ { type: :buttons, buttons: [
19
+ { type: :copy_code, example: "SPRING25" },
20
+ { type: :url, text: "Book now!", url: "https://example.com/o?c={{1}}", example: "n3mtql" },
21
+ ] },
22
+ ]
23
+ )
24
+ ```
25
+
26
+ ## The component
27
+
28
+ | Field | Required | Rules |
29
+ | --- | --- | --- |
30
+ | `text` | yes | Max **16** characters — the offer label |
31
+ | `has_expiration` | no | `true` shows the countdown |
32
+
33
+ It serializes nested, unlike every other component:
34
+
35
+ ```ruby
36
+ { type: "LIMITED_TIME_OFFER", limited_time_offer: { text: "Expiring offer!", has_expiration: true } }
37
+ ```
38
+
39
+ ## The knock-on rules
40
+
41
+ Adding a limited-time offer changes what the rest of the template may contain. These
42
+ are enforced by `ComponentSet`, which is the only object that can see all the
43
+ components at once:
44
+
45
+ | Rule | Error if violated |
46
+ | --- | --- |
47
+ | **No `FOOTER`** at all | `a FOOTER is not allowed in a LIMITED_TIME_OFFER template` |
48
+ | `BODY` drops to **600** chars (from 1024) | `the BODY of a LIMITED_TIME_OFFER template is limited to 600 characters` |
49
+ | Header must be **`IMAGE` or `VIDEO`** | `the header must be IMAGE or VIDEO in a LIMITED_TIME_OFFER template, got TEXT` |
50
+ | Copy-code button must be **first** | `the copy-code button must be first in a LIMITED_TIME_OFFER template` |
51
+ | Copy-code `example` ≤ **15** chars (from 20) | `the copy-code example is limited to 15 characters in a LIMITED_TIME_OFFER template` |
52
+
53
+ ## Serialized payload
54
+
55
+ ```ruby
56
+ Whatsapp::MessageTemplates::Template.new(
57
+ name: "limited_time_offer_caribbean_pkg_2023", language: "en_US", category: "marketing",
58
+ components: [
59
+ { type: :header, format: "image", header_handle: "4::aW" },
60
+ { type: :limited_time_offer, text: "Expiring offer!", has_expiration: true },
61
+ { type: :body, text: "Good news, {{1}}! Use code {{2}} to get 25% off!",
62
+ example: ["Pablo", "CARIBE25"] },
63
+ { type: :buttons, buttons: [
64
+ { type: :copy_code, example: "CARIBE25" },
65
+ { type: :url, text: "Book now!", url: "https://x.test/offers?code={{1}}", example: "n3mtql" },
66
+ ] },
67
+ ]
68
+ ).serialize
69
+ # => {
70
+ # name: "limited_time_offer_caribbean_pkg_2023", language: "en_US", category: "MARKETING",
71
+ # parameter_format: "POSITIONAL",
72
+ # components: [
73
+ # { type: "HEADER", format: "IMAGE", example: { header_handle: ["4::aW"] } },
74
+ # { type: "LIMITED_TIME_OFFER",
75
+ # limited_time_offer: { text: "Expiring offer!", has_expiration: true } },
76
+ # { type: "BODY", text: "Good news, {{1}}! Use code {{2}} to get 25% off!",
77
+ # example: { body_text: [["Pablo", "CARIBE25"]] } },
78
+ # { type: "BUTTONS", buttons: [
79
+ # { type: "COPY_CODE", example: "CARIBE25" },
80
+ # { type: "URL", text: "Book now!", url: "https://x.test/offers?code={{1}}",
81
+ # example: ["n3mtql"] }] },
82
+ # ]
83
+ # }
84
+ ```
85
+
86
+ ## Media handles
87
+
88
+ `header_handle` comes from Meta's Resumable Upload API, not
89
+ [`Media#upload`](../media/README.md) — see
90
+ [README.md § Not wrapped](README.md#not-wrapped).
91
+
92
+ ---
93
+
94
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/limited-time-offer-templates>
@@ -0,0 +1,173 @@
1
+ # Template Responses
2
+
3
+ Four endpoints, four response shapes — so four classes rather than one that is mostly
4
+ `nil`.
5
+
6
+ | Method | Returns |
7
+ | --- | --- |
8
+ | `create`, `create_from_library`, `upsert` | [`Response::Created`](#created) |
9
+ | `find` | [`Response::Node`](#node) |
10
+ | `list` | [`Response::Collection`](#collection) |
11
+ | `update`, `delete` | plain `Boolean` |
12
+
13
+ ## Created
14
+
15
+ `{id, status, category}` — what you get back from a submission.
16
+
17
+ ```ruby
18
+ created = templates.create(name: "order_confirmation", language: "en_US",
19
+ category: "UTILITY", components: [...])
20
+
21
+ created.id # => "1259544702043867"
22
+ created.status # => "PENDING"
23
+ created.category # => "UTILITY"
24
+
25
+ created.pending? # => true
26
+ created.approved? # => false
27
+ created.rejected? # => false
28
+ ```
29
+
30
+ A [library template](library.md) usually comes back `APPROVED` immediately; everything
31
+ else starts `PENDING` and is reviewed asynchronously.
32
+
33
+ ## Node
34
+
35
+ The full template object, from `find`.
36
+
37
+ ```ruby
38
+ template = templates.find(template_id: "564750795574598")
39
+ ```
40
+
41
+ | Accessor | Example |
42
+ | --- | --- |
43
+ | `id` | `"564750795574598"` |
44
+ | `name` | `"order_confirmation"` |
45
+ | `status` | `"APPROVED"` |
46
+ | `category` | `"UTILITY"` |
47
+ | `language` | `"en_US"` |
48
+ | `components` | **raw hashes** — see below |
49
+ | `parameter_format` | `"POSITIONAL"` |
50
+ | `sub_category` | `"ORDER_DETAILS"` |
51
+ | `rejected_reason` | `"NONE"` |
52
+ | `quality_score` | `#<QualityScore score="GREEN" date=1700000000 reasons=[]>` |
53
+ | `previous_category` | `"MARKETING"` |
54
+ | `correct_category` | `"UTILITY"` |
55
+ | `message_send_ttl_seconds` | `3600` |
56
+ | `cta_url_link_tracking_opted_out` | `false` |
57
+ | `library_template_name` | `"delivery_update_1"` |
58
+
59
+ Predicates: `#approved?`, `#rejected?`, `#paused?`, and `#editable?` (true for
60
+ `APPROVED`, `REJECTED`, and `PAUSED`).
61
+
62
+ ```ruby
63
+ template.status # => "APPROVED"
64
+ template.editable? # => true
65
+ ```
66
+
67
+ > **`#components` stays raw hashes on purpose.** It is not rebuilt into the
68
+ > `Component` classes. Those validate a payload being *written*; this is Meta's echo of
69
+ > one, carrying fields the write side does not model. Running write-side rules over
70
+ > data we did not author would raise on perfectly valid responses. Read it as data;
71
+ > build a new `Template` when you want to change something.
72
+
73
+ ```ruby
74
+ template.components
75
+ # => [{ "type" => "BODY", "text" => "Thank you, {{1}}!",
76
+ # "example" => { "body_text" => [["Pablo"]] } }]
77
+ ```
78
+
79
+ ## Collection
80
+
81
+ `{data, paging, summary}` from `list`. It is `Enumerable`, so it behaves like the array
82
+ of `Node`s it wraps.
83
+
84
+ ```ruby
85
+ page = templates.list(status: %w[APPROVED], limit: 25)
86
+
87
+ page.map(&:name) # => ["order_confirmation", ...]
88
+ page.select(&:approved?) # Enumerable
89
+ page.next_cursor # => "AFTER" — nil on the last page
90
+ page.remaining # => 249
91
+ ```
92
+
93
+ | Accessor | Holds |
94
+ | --- | --- |
95
+ | `data` | `Array<Response::Node>` |
96
+ | `paging` | `Response::Paging` — `before`, `after` (flattened from `paging.cursors`) |
97
+ | `summary` | `Response::Summary` |
98
+
99
+ `#next_cursor` is `paging&.after`; `#remaining` is `summary&.remaining`.
100
+
101
+ ### Summary
102
+
103
+ | Accessor | Meaning |
104
+ | --- | --- |
105
+ | `total_count` | Templates matching the filter |
106
+ | `message_template_count` | Templates on the account |
107
+ | `message_template_limit` | The account's cap |
108
+ | `are_translations_complete` | Boolean |
109
+ | `remaining` | `limit - count`, or `nil` when either is missing |
110
+
111
+ ```ruby
112
+ page.summary.message_template_count # => 12
113
+ page.summary.message_template_limit # => 250
114
+ page.remaining # => 238
115
+ ```
116
+
117
+ ### Paging
118
+
119
+ Manual — there is no auto-pagination:
120
+
121
+ ```ruby
122
+ page = templates.list(limit: 25)
123
+ all = page.to_a
124
+
125
+ while (cursor = page.next_cursor)
126
+ page = templates.list(limit: 25, after: cursor)
127
+ all.concat(page.to_a)
128
+ end
129
+ ```
130
+
131
+ ### QualityScore
132
+
133
+ ```ruby
134
+ template.quality_score.score # => "GREEN" — GREEN | YELLOW | RED | UNKNOWN
135
+ template.quality_score.date # => 1_700_000_000
136
+ template.quality_score.reasons # => []
137
+ ```
138
+
139
+ Best-effort schema on `reasons` — Meta publishes no sub-fields for it.
140
+
141
+ ## Booleans
142
+
143
+ `update` and `delete` return a plain `true`/`false` derived from `{"success": true}`:
144
+
145
+ ```ruby
146
+ templates.update(template_id: "564750795574598", category: "MARKETING") # => true
147
+ templates.delete(name: "order_confirmation") # => true
148
+ ```
149
+
150
+ A non-2xx response raises `Whatsapp::MessageTemplates::TemplateError` instead of
151
+ returning `false`, so `false` genuinely means "Meta said no", not "the request failed".
152
+
153
+ ## Statuses
154
+
155
+ | Status | Meaning |
156
+ | --- | --- |
157
+ | `PENDING` | Under review |
158
+ | `APPROVED` | Sendable |
159
+ | `REJECTED` | Refused — see `rejected_reason` |
160
+ | `PAUSED` | Temporarily halted for quality |
161
+ | `DISABLED` | Permanently disabled; cannot be deleted |
162
+ | `IN_APPEAL` | Rejection under appeal |
163
+ | `PENDING_DELETION` | Deletion queued |
164
+ | `DELETED` | Gone |
165
+ | `LIMIT_EXCEEDED` | Account template cap hit |
166
+ | `ARCHIVED` | Archived |
167
+
168
+ `rejected_reason` is one of `NONE`, `ABUSIVE_CONTENT`, `INVALID_FORMAT`,
169
+ `PROMOTIONAL`, `TAG_CONTENT_MISMATCH`, `SCAM`.
170
+
171
+ ---
172
+
173
+ **Meta docs:** <https://developers.facebook.com/docs/graph-api/reference/whats-app-business-hsm/>
@@ -0,0 +1,138 @@
1
+ # Standard Templates
2
+
3
+ Utility and marketing templates built from text components — the ordinary case. An
4
+ order confirmation, a shipping update, a password-reset notice.
5
+
6
+ Every standard template needs **exactly one `BODY`**; everything else is optional.
7
+
8
+ ```ruby
9
+ templates = Whatsapp::MessageTemplates.new
10
+
11
+ created = templates.create(
12
+ name: "order_confirmation", # lowercase alphanumerics and underscores only
13
+ language: "en_US",
14
+ category: "UTILITY", # UTILITY | MARKETING | AUTHENTICATION
15
+ components: [
16
+ { type: :header, format: "TEXT", text: "Order {{1}} confirmed", example: ["#1234"] },
17
+ { type: :body,
18
+ text: "Thank you, {{1}}! Your order number is {{2}}.",
19
+ example: ["Pablo", "860198-230332"] },
20
+ { type: :footer, text: "Thanks for shopping with us" },
21
+ { type: :buttons, buttons: [
22
+ { type: :phone_number, text: "Call", phone_number: "15550051310" },
23
+ { type: :url, text: "Track order", url: "https://example.com/orders/{{1}}", example: "1234" },
24
+ ] },
25
+ ]
26
+ )
27
+
28
+ created.id # => "1259544702043867"
29
+ created.status # => "PENDING" — Meta reviews asynchronously, up to 24 hours
30
+ created.pending? # => true
31
+ ```
32
+
33
+ ## Placeholders and examples
34
+
35
+ A placeholder is `{{1}}` (positional) or `{{first_name}}` (named). Meta requires a
36
+ sample value for every one of them, so a human reviewer can see what the template
37
+ actually says. The gem checks the count matches **before** the request:
38
+
39
+ ```ruby
40
+ templates.create(
41
+ name: "x", language: "en_US", category: "UTILITY",
42
+ components: [{ type: :body, text: "Hi {{1}} and {{2}}", example: ["Pablo"] }]
43
+ )
44
+ # => ActiveModel::ValidationError: Example does not match the body text:
45
+ # 2 placeholders but 1 example
46
+ ```
47
+
48
+ Positional placeholders must **start at `{{1}}` and increment without gaps**, and you
49
+ cannot mix the two styles in one template.
50
+
51
+ > **Ruby gotcha.** Meta's own examples include text like `Your order #{{2}}`. In a
52
+ > double-quoted Ruby string, `#{` starts interpolation — use single quotes or escape
53
+ > it:
54
+ >
55
+ > ```ruby
56
+ > text: 'Your order #{{2}} has shipped' # single quotes
57
+ > text: "Your order \#{{2}} has shipped" # or escape
58
+ > ```
59
+
60
+ ### Named parameters
61
+
62
+ Named parameters read better than positional ones for anything non-trivial. Set
63
+ `parameter_format: "NAMED"` and pass a Hash of name ⇒ example:
64
+
65
+ ```ruby
66
+ templates.create(
67
+ name: "order_confirmation", language: "en_US", category: "UTILITY",
68
+ parameter_format: "NAMED",
69
+ components: [
70
+ { type: :body,
71
+ text: "Thank you, {{first_name}}! Your order number is {{order_number}}.",
72
+ example: { first_name: "Pablo", order_number: "860198-230332" } },
73
+ ]
74
+ )
75
+ ```
76
+
77
+ ### The `example` key trap
78
+
79
+ The `example` key **name** changes with both the component and the format. This is the
80
+ single most rejection-prone detail in the API, so the gem builds it for you — you pass
81
+ the values, it picks the key:
82
+
83
+ | Role | `POSITIONAL` | `NAMED` |
84
+ | --- | --- | --- |
85
+ | header | `header_text: ["Sale"]` (flat) | `header_text_named_params: [{param_name:, example:}]` |
86
+ | body | `body_text: [["a","b"]]` (**nested**) | `body_text_named_params: [{param_name:, example:}]` |
87
+
88
+ Pass whichever input shape suits you — an `Array` (positional), a `Hash` of
89
+ name ⇒ example (named), Meta's own `[{param_name:, example:}]`, or a fully-built
90
+ payload hash copied straight out of the docs.
91
+
92
+ ## Serialized payload
93
+
94
+ ```ruby
95
+ Whatsapp::MessageTemplates::Template.new(
96
+ name: "order_confirmation", language: "en_US", category: "utility",
97
+ components: [
98
+ { type: :header, format: "DOCUMENT", header_handle: "4::YX" },
99
+ { type: :body,
100
+ text: "Thank you for your order, {{1}}! Your order number is {{2}}.",
101
+ example: ["Pablo", "860198-230332"] },
102
+ { type: :buttons, buttons: [
103
+ { type: :phone_number, text: "Call", phone_number: "15550051310" },
104
+ { type: :url, text: "Contact Support", url: "https://www.luckyshrub.com/support" },
105
+ ] },
106
+ ]
107
+ ).serialize
108
+ # => {
109
+ # name: "order_confirmation", language: "en_US", category: "UTILITY",
110
+ # parameter_format: "POSITIONAL",
111
+ # components: [
112
+ # { type: "HEADER", format: "DOCUMENT", example: { header_handle: ["4::YX"] } },
113
+ # { type: "BODY", text: "Thank you for your order, {{1}}! Your order number is {{2}}.",
114
+ # example: { body_text: [["Pablo", "860198-230332"]] } },
115
+ # { type: "BUTTONS", buttons: [
116
+ # { type: "PHONE_NUMBER", text: "Call", phone_number: "15550051310" },
117
+ # { type: "URL", text: "Contact Support", url: "https://www.luckyshrub.com/support" }] },
118
+ # ]
119
+ # }
120
+ ```
121
+
122
+ Note the lowercase `category: "utility"` on input and `"UTILITY"` on the wire — every
123
+ enum accepts either casing and emits uppercase.
124
+
125
+ ## Rules worth remembering
126
+
127
+ | Component | Limit |
128
+ | --- | --- |
129
+ | `header` (TEXT) | ≤ **60** chars, **at most one** placeholder, no Markdown (`*` `_` `~` `` ` ``) |
130
+ | `body` | ≤ **1024** chars, any number of placeholders, matching example required |
131
+ | `footer` | ≤ **60** chars and **no placeholders** |
132
+ | `buttons` | 1–**10** buttons; quick-replies must be **contiguous** |
133
+
134
+ Full reference in [components.md](components.md).
135
+
136
+ ---
137
+
138
+ **Meta docs:** <https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates/components/>
@@ -0,0 +1,135 @@
1
+ # Sending Messages
2
+
3
+ Everything in this directory builds an *outbound* payload for
4
+ `POST /{PHONE_ID}/messages`. Each WhatsApp message kind is its own
5
+ `ActiveModel`-validated Ruby class, so a payload Meta would reject comes back as a
6
+ local `ActiveModel::ValidationError` instead of a round trip and an opaque
7
+ `#131009`.
8
+
9
+ Addresses **`phone_id`**, not `waba_id`, and needs the
10
+ `whatsapp_business_messaging` permission.
11
+
12
+ ## The two ways to send
13
+
14
+ Every kind registered in `Whatsapp::Messages::KINDS` gets a generated
15
+ `Whatsapp::Messages.send_<kind>!` class method — pass the recipient and the
16
+ kind-specific fields as keyword arguments:
17
+
18
+ ```ruby
19
+ Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello!")
20
+ # => #<Whatsapp::Messages::Response messaging_product="whatsapp" ...>
21
+ ```
22
+
23
+ If the kind is only known at runtime, drop down to the factory those methods are
24
+ built on:
25
+
26
+ ```ruby
27
+ Whatsapp::Messages.new(
28
+ kind: :text, # or a variable; String or Symbol
29
+ payload: { to: "+15551234567", body: "Hello!" },
30
+ client: Whatsapp::Client.new # optional
31
+ ).send!
32
+ ```
33
+
34
+ Both forms accept an optional `client:` (defaults to a new `Whatsapp::Client`
35
+ built from `Whatsapp.configuration`) and return a
36
+ [`Whatsapp::Messages::Response`](#the-response).
37
+
38
+ ## The kinds
39
+
40
+ | Kind | Method | Sends |
41
+ | --- | --- | --- |
42
+ | `:text` | [`send_text!`](text.md) | Plain text with an optional link preview |
43
+ | `:image` | [`send_image!`](image.md) | An image with an optional caption |
44
+ | `:video` | [`send_video!`](video.md) | A video with an optional caption |
45
+ | `:audio` | [`send_audio!`](audio.md) | A voice note or audio clip |
46
+ | `:document` | [`send_document!`](document.md) | A file with an optional caption and filename |
47
+ | `:sticker` | [`send_sticker!`](sticker.md) | A sticker |
48
+ | `:contacts` | [`send_contacts!`](contacts.md) | A rich, vCard-like contact card |
49
+ | `:reaction` | [`send_reaction!`](reaction.md) | An emoji reaction to a previous message |
50
+ | `:location` | [`send_location!`](location.md) | A latitude/longitude pin |
51
+ | `:address` | [`send_address!`](address.md) | A delivery-address form (India & Singapore only) |
52
+ | `:location_request` | [`send_location_request!`](location_request.md) | A prompt asking the user to share their location |
53
+ | `:template` | [`send_template!`](template.md) | A pre-approved marketing/utility/authentication template |
54
+ | `:interactive` | [`send_interactive!`](interactive.md) | Reply buttons, lists, CTA URLs, or carousels |
55
+
56
+ Plus one endpoint that isn't a message kind at all:
57
+
58
+ | | Method | Does |
59
+ | --- | --- | --- |
60
+ | — | [`mark_message_as_read!`](mark_message_as_read.md) | Closes the read-receipt loop on an inbound message |
61
+
62
+ ## The shared envelope
63
+
64
+ Every kind except `mark_message_as_read` inherits `Messages::Base`, which supplies
65
+ `to` (the only universally required field, validated for presence) and wraps the
66
+ kind-specific hash in a common envelope:
67
+
68
+ ```ruby
69
+ {
70
+ messaging_product: "whatsapp",
71
+ recipient_type: "individual",
72
+ to: "+15551234567",
73
+ type: "<kind>",
74
+ "<kind>": { ... } # the kind-specific payload
75
+ }
76
+ ```
77
+
78
+ Three kinds break the `type == kind` symmetry because Meta transports them as
79
+ interactive messages: [`address`](address.md), [`location_request`](location_request.md),
80
+ and of course [`interactive`](interactive.md) all serialize with `type: "interactive"`.
81
+
82
+ ## The response
83
+
84
+ A successful `send!` returns a `Whatsapp::Messages::Response`, deserialized from a
85
+ body shaped like:
86
+
87
+ ```json
88
+ {
89
+ "messaging_product": "whatsapp",
90
+ "contacts": [{ "input": "+15551234567", "wa_id": "15551234567" }],
91
+ "messages": [{ "id": "wamid.HBgLMTU1NTU1NTU1NTUV..." }]
92
+ }
93
+ ```
94
+
95
+ ```ruby
96
+ response = Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi")
97
+
98
+ response.messaging_product # => "whatsapp"
99
+ response.messages.first.id # => "wamid.HBgLMTU1NTU1NTU1NTUV..."
100
+ response.contacts.first.input # => "+15551234567"
101
+ response.contacts.first.wa_id # => "15551234567"
102
+ response.success # => nil for a normal send
103
+ ```
104
+
105
+ `#success` is `nil` for a normal message send and `true`/`false` only for the
106
+ status-update endpoints that reply `{"success": true}` — today just
107
+ [`mark_message_as_read!`](mark_message_as_read.md).
108
+
109
+ ## Errors
110
+
111
+ | Raised | When |
112
+ | --- | --- |
113
+ | `ArgumentError` | `to:` (or another required keyword) is missing |
114
+ | `ActiveModel::ValidationError` | A field fails a local validation |
115
+ | `Whatsapp::Messages::PayloadError` | `kind:` is unrecognized — only reachable through the `Messages.new(kind:, payload:)` form |
116
+ | `Whatsapp::RequestError` | The API answered with a non-2xx status |
117
+
118
+ Because every kind runs `validate!` at the end of its own `initialize`, an invalid
119
+ field raises at *construction* time, before a client is ever touched:
120
+
121
+ ```ruby
122
+ Whatsapp::Messages.send_image!(to: "+15551234567")
123
+ # => ActiveModel::ValidationError: Either id or link must be present
124
+
125
+ Whatsapp::Messages.new(kind: :telepathy, payload: { to: "+1" })
126
+ # => Whatsapp::Messages::PayloadError: Unknown message kind: :telepathy.
127
+ # Known kinds: text, image, audio, video, document, sticker, contacts,
128
+ # reaction, location, address, location_request, template, interactive
129
+ ```
130
+
131
+ See [../errors.md](../errors.md) for the full hierarchy.
132
+
133
+ ---
134
+
135
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/text-messages>
@@ -0,0 +1,86 @@
1
+ # Address Messages
2
+
3
+ Collects or confirms a structured delivery address through a native WhatsApp form,
4
+ instead of parsing a free-text reply.
5
+
6
+ > **India and Singapore only.** This is a Meta feature-availability restriction on the
7
+ > WhatsApp Business Account itself, not merely a payload field. A WABA outside those
8
+ > two markets gets `(#131009) Unsupported Interactive Message type` — that is expected,
9
+ > not a bug in this class.
10
+
11
+ ```ruby
12
+ Whatsapp::Messages.send_address!(
13
+ to: "+15551234567",
14
+ body: "Please share your delivery address",
15
+ country: "IN"
16
+ )
17
+ ```
18
+
19
+ ## Fields
20
+
21
+ | Field | Required | Rules |
22
+ | --- | --- | --- |
23
+ | `to` | yes | The recipient's phone number |
24
+ | `body` | yes | The prompt shown above the form |
25
+ | `country` | yes | `"IN"` or `"SG"` only |
26
+ | `footer` | no | Small print under the form |
27
+ | `values` | no | Hash pre-filling form fields |
28
+ | `saved_addresses` | no | Array of previously saved addresses to offer. Defaults to `[]` |
29
+
30
+ `values` pre-fills the form so the customer edits rather than types. `saved_addresses`
31
+ offers a pick list of addresses you already hold for them:
32
+
33
+ ```ruby
34
+ Whatsapp::Messages.send_address!(
35
+ to: "+15551234567",
36
+ body: "Please share your delivery address",
37
+ country: "IN",
38
+ footer: "Thanks for shopping with us",
39
+ values: { name: "Jane Doe", phone_number: "+15550001111", city: "Bangalore" },
40
+ saved_addresses: [
41
+ { id: "addr_1", name: "Jane Doe", pin_code: "560001",
42
+ address: "12 MG Road", city: "Bangalore", state: "KA" },
43
+ ]
44
+ )
45
+ ```
46
+
47
+ ## Serialized payload
48
+
49
+ Like [Location Request](location_request.md), this travels as an interactive message:
50
+
51
+ ```ruby
52
+ Whatsapp::Messages::Address.new(
53
+ to: "+15551234567",
54
+ body: "Please share your delivery address",
55
+ country: "IN",
56
+ footer: "Thanks for shopping with us",
57
+ values: { name: "Jane Doe", city: "Bangalore" }
58
+ ).serialize
59
+ # => {
60
+ # messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567",
61
+ # type: "interactive",
62
+ # interactive: {
63
+ # type: "address_message",
64
+ # body: { text: "Please share your delivery address" },
65
+ # action: {
66
+ # name: "address_message",
67
+ # parameters: { country: "IN", values: { name: "Jane Doe", city: "Bangalore" } }
68
+ # },
69
+ # footer: { text: "Thanks for shopping with us" }
70
+ # }
71
+ # }
72
+ ```
73
+
74
+ `values` is included only when given; `saved_addresses` only when non-empty; `footer`
75
+ only when truthy.
76
+
77
+ ## Validation errors
78
+
79
+ ```ruby
80
+ Whatsapp::Messages.send_address!(to: "+15551234567", body: "Address?", country: "US")
81
+ # => ActiveModel::ValidationError: Country is not included in the list
82
+ ```
83
+
84
+ ---
85
+
86
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/address-messages>
@@ -0,0 +1,43 @@
1
+ # Audio Messages
2
+
3
+ Sends a voice note or audio clip.
4
+
5
+ ```ruby
6
+ Whatsapp::Messages.send_audio!(to: "+15551234567", link: "https://example.com/clip.mp3")
7
+ ```
8
+
9
+ ## Fields
10
+
11
+ | Field | Required | Rules |
12
+ | --- | --- | --- |
13
+ | `to` | yes | The recipient's phone number |
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.**
18
+
19
+ > **No caption.** Unlike [image](image.md), [video](video.md), and
20
+ > [document](document.md), the Cloud API has no caption field for audio, so the class
21
+ > does not accept one. Send a separate [text message](text.md) if you need
22
+ > accompanying copy.
23
+
24
+ ## Serialized payload
25
+
26
+ ```ruby
27
+ Whatsapp::Messages::Audio.new(to: "+15551234567", link: "https://example.com/clip.mp3").serialize
28
+ # => {
29
+ # messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "audio",
30
+ # audio: { link: "https://example.com/clip.mp3" }
31
+ # }
32
+ ```
33
+
34
+ ## Validation errors
35
+
36
+ ```ruby
37
+ Whatsapp::Messages.send_audio!(to: "+15551234567")
38
+ # => ActiveModel::ValidationError: Either id or link must be present
39
+ ```
40
+
41
+ ---
42
+
43
+ **Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/audio-messages>