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,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>
|