ruby-whatsapp 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -0
- data/README.md +322 -617
- data/docs/README.md +76 -0
- data/docs/business_phone_number/README.md +243 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/contact_book.md +90 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/business_phone_number/username.md +157 -0
- data/docs/configuration.md +177 -0
- data/docs/errors.md +219 -0
- data/docs/media/README.md +161 -0
- data/docs/message_templates/README.md +206 -0
- data/docs/message_templates/authentication.md +107 -0
- data/docs/message_templates/carousel.md +103 -0
- data/docs/message_templates/components.md +215 -0
- data/docs/message_templates/library.md +98 -0
- data/docs/message_templates/limited_time_offer.md +94 -0
- data/docs/message_templates/responses.md +173 -0
- data/docs/message_templates/standard.md +138 -0
- data/docs/messages/README.md +212 -0
- data/docs/messages/address.md +86 -0
- data/docs/messages/audio.md +43 -0
- data/docs/messages/contacts.md +129 -0
- data/docs/messages/document.md +49 -0
- data/docs/messages/image.md +59 -0
- data/docs/messages/interactive.md +342 -0
- data/docs/messages/location.md +51 -0
- data/docs/messages/location_request.md +59 -0
- data/docs/messages/mark_message_as_read.md +84 -0
- data/docs/messages/reaction.md +59 -0
- data/docs/messages/sticker.md +40 -0
- data/docs/messages/template.md +193 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/schedule/README.md +183 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +217 -0
- data/docs/webhooks/account_alerts.md +44 -0
- data/docs/webhooks/account_review_update.md +29 -0
- data/docs/webhooks/account_update.md +54 -0
- data/docs/webhooks/automatic_events.md +47 -0
- data/docs/webhooks/business_capability_update.md +40 -0
- data/docs/webhooks/business_username_updates.md +55 -0
- data/docs/webhooks/history.md +107 -0
- data/docs/webhooks/message_template_components_update.md +46 -0
- data/docs/webhooks/message_template_quality_update.md +49 -0
- data/docs/webhooks/message_template_status_update.md +55 -0
- data/docs/webhooks/messages.md +520 -0
- data/docs/webhooks/partner_solutions.md +35 -0
- data/docs/webhooks/payment_configuration_update.md +40 -0
- data/docs/webhooks/phone_number_name_update.md +44 -0
- data/docs/webhooks/phone_number_quality_update.md +40 -0
- data/docs/webhooks/security.md +43 -0
- data/docs/webhooks/smb_app_state_sync.md +91 -0
- data/docs/webhooks/smb_message_echoes.md +91 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_id_update.md +84 -0
- data/docs/webhooks/user_preferences.md +87 -0
- data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
- data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
- data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
- data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
- data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
- data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
- data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
- data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
- data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
- data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
- data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
- data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
- data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
- data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
- data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
- data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/messages/base.rb +47 -5
- data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
- data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
- data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/schedule/create.rb +101 -0
- data/lib/ruby/whatsapp/schedule/list.rb +50 -0
- data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
- data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
- data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
- data/lib/ruby/whatsapp/schedule/response.rb +18 -0
- data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
- data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
- data/lib/ruby/whatsapp/schedule.rb +15 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
- data/lib/ruby/whatsapp/webhook/change.rb +3 -1
- data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
- data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
- data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
- data/lib/ruby/whatsapp/webhook/history.rb +73 -34
- data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
- data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
- data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
- data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
- data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
- data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
- data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
- data/lib/ruby/whatsapp/webhook/message.rb +3 -0
- data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
- data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
- data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
- data/lib/ruby/whatsapp/webhook/status.rb +23 -2
- data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
- data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
- data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
- data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
- data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
- metadata +100 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Template Messages
|
|
2
|
+
|
|
3
|
+
The **only** way to message a user outside the 24-hour customer-service window.
|
|
4
|
+
Marketing, utility, and authentication templates must be created and approved by Meta
|
|
5
|
+
before they can be sent.
|
|
6
|
+
|
|
7
|
+
> **Two different APIs.** This page covers *sending* an already-approved template
|
|
8
|
+
> (`POST /{PHONE_ID}/messages`). Creating, editing, and deleting the templates
|
|
9
|
+
> themselves is a separate API addressing `waba_id` — see
|
|
10
|
+
> [../message_templates/](../message_templates/README.md). The component schemas are
|
|
11
|
+
> **not** interchangeable: management uses `text` + `example` with uppercase
|
|
12
|
+
> `HEADER`/`BODY`/`FOOTER`, sending uses `parameters` with lowercase singular types.
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
Whatsapp::Messages.send_template!(
|
|
16
|
+
to: "+15551234567",
|
|
17
|
+
name: "order_confirmation",
|
|
18
|
+
language: { code: "en_US" }
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Fields
|
|
23
|
+
|
|
24
|
+
| Field | Required | Rules |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
27
|
+
| `name` | yes | The approved template's name |
|
|
28
|
+
| `language` | yes | `{ code: "en_US" }` — validated against the supported list |
|
|
29
|
+
| `components` | no | Array of components supplying placeholder values. Defaults to `[]` |
|
|
30
|
+
|
|
31
|
+
`language.code` is checked against `Whatsapp::Utils::LanguageCodes` (71 codes), so a
|
|
32
|
+
typo raises locally instead of coming back as a Meta error:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Whatsapp::Utils::LanguageCodes.valid?("en_US") # => true
|
|
36
|
+
Whatsapp::Utils::LanguageCodes.all.size # => 71
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Authentication templates cannot go to a BSUID
|
|
40
|
+
|
|
41
|
+
One-tap, zero-tap, and copy-code
|
|
42
|
+
[authentication templates](../message_templates/authentication.md) require the
|
|
43
|
+
recipient's **phone number**. Address one to a
|
|
44
|
+
[BSUID](README.md#business-scoped-user-ids-bsuid) via `recipient:` and Meta answers
|
|
45
|
+
with error `131062`:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Business-scoped User ID (BSUID) recipients are not supported for this message.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The gem cannot catch this locally, and deliberately does not try. A template is
|
|
52
|
+
addressed at send time by `name` and `language` only — `Messages::Template` never
|
|
53
|
+
learns its own category, and the OTP configuration lives in the WABA. Guessing from
|
|
54
|
+
`components` would be worse than not guessing: a `copy_code` sub-type appears both on
|
|
55
|
+
copy-code authentication templates *and* on limited-time-offer marketing templates,
|
|
56
|
+
which have no such restriction, so any local rule would reject valid sends.
|
|
57
|
+
|
|
58
|
+
If you need to reach a username-only user with an authentication template, ask for
|
|
59
|
+
their phone number first — see the `REQUEST_CONTACT_INFO` button in
|
|
60
|
+
[components.md](../message_templates/components.md).
|
|
61
|
+
|
|
62
|
+
> Meta names one-tap, zero-tap, and copy-code specifically. Whether a plain
|
|
63
|
+
> `NO_BUTTONS` authentication template is also refused is **not documented** — treat it
|
|
64
|
+
> as untested rather than assuming either way.
|
|
65
|
+
|
|
66
|
+
## Components
|
|
67
|
+
|
|
68
|
+
A component supplies the *values* for the placeholders the template defines. It has a
|
|
69
|
+
`type`, and for buttons a `sub_type` and `index`.
|
|
70
|
+
|
|
71
|
+
| `type` | Purpose |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `"header"` | Values for the header's placeholder |
|
|
74
|
+
| `"body"` | Values for the body's placeholders |
|
|
75
|
+
| `"button"` | Values for a dynamic button — also needs `sub_type` and `index` |
|
|
76
|
+
|
|
77
|
+
Each component holds `parameters`, and each parameter has a `type` selecting exactly
|
|
78
|
+
one value key:
|
|
79
|
+
|
|
80
|
+
| Parameter `type` | Value key | Shape |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `"text"` | `text` | String |
|
|
83
|
+
| `"currency"` | `currency` | `{ fallback_value:, code:, amount_1000: }` |
|
|
84
|
+
| `"date_time"` | `date_time` | `{ fallback_value: }` |
|
|
85
|
+
| `"image"` | `image` | `{ link: }` or `{ id: }` |
|
|
86
|
+
| `"document"` | `document` | `{ link: }` or `{ id: }` |
|
|
87
|
+
| `"video"` | `video` | `{ link: }` or `{ id: }` |
|
|
88
|
+
| `"location"` | `location` | `{ latitude:, longitude:, name:, address: }` |
|
|
89
|
+
| `"payload"` | `payload` | String — a quick-reply button's payload |
|
|
90
|
+
|
|
91
|
+
> Only `type` is validated. There is no cross-field check that the matching value key
|
|
92
|
+
> is populated, so `{ type: "text" }` with no `text` serializes to a bare
|
|
93
|
+
> `{ type: "text" }` and is rejected by Meta rather than locally.
|
|
94
|
+
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
**Body placeholders:**
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
Whatsapp::Messages.send_template!(
|
|
101
|
+
to: "+15551234567",
|
|
102
|
+
name: "order_confirmation",
|
|
103
|
+
language: { code: "en_US" },
|
|
104
|
+
components: [
|
|
105
|
+
{ type: "body", parameters: [{ type: "text", text: "Jane" },
|
|
106
|
+
{ type: "text", text: "860198-230332" }] },
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Media header plus body:**
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
Whatsapp::Messages.send_template!(
|
|
115
|
+
to: "+15551234567",
|
|
116
|
+
name: "shipping_update",
|
|
117
|
+
language: { code: "en_US" },
|
|
118
|
+
components: [
|
|
119
|
+
{ type: "header", parameters: [{ type: "image", image: { link: "https://example.com/box.jpg" } }] },
|
|
120
|
+
{ type: "body", parameters: [{ type: "text", text: "Jane" }] },
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**A dynamic URL button** — `sub_type` and `index` identify which button in the
|
|
126
|
+
template's button row is being filled:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
Whatsapp::Messages.send_template!(
|
|
130
|
+
to: "+15551234567",
|
|
131
|
+
name: "order_confirmation",
|
|
132
|
+
language: { code: "en_US" },
|
|
133
|
+
components: [
|
|
134
|
+
{ type: "body", parameters: [{ type: "text", text: "Jane" }] },
|
|
135
|
+
{ type: "button", sub_type: "url", index: 0,
|
|
136
|
+
parameters: [{ type: "text", text: "1234" }] },
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**A currency and a date:**
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
components: [
|
|
145
|
+
{ type: "body", parameters: [
|
|
146
|
+
{ type: "currency", currency: { fallback_value: "$100.99", code: "USD", amount_1000: 100_990 } },
|
|
147
|
+
{ type: "date_time", date_time: { fallback_value: "February 25, 1977" } },
|
|
148
|
+
] },
|
|
149
|
+
]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Serialized payload
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Whatsapp::Messages::Template.new(
|
|
156
|
+
to: "+15551234567",
|
|
157
|
+
name: "order_confirmation",
|
|
158
|
+
language: { code: "en_US" },
|
|
159
|
+
components: [
|
|
160
|
+
{ type: "body", parameters: [{ type: "text", text: "Jane" }, { type: "text", text: "#1234" }] },
|
|
161
|
+
]
|
|
162
|
+
).serialize
|
|
163
|
+
# => {
|
|
164
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "template",
|
|
165
|
+
# template: {
|
|
166
|
+
# name: "order_confirmation",
|
|
167
|
+
# language: { code: "en_US" },
|
|
168
|
+
# components: [{ type: "body", parameters: [{ type: "text", text: "Jane" },
|
|
169
|
+
# { type: "text", text: "#1234" }] }]
|
|
170
|
+
# }
|
|
171
|
+
# }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
With no components, the `components` key is dropped entirely rather than sent empty.
|
|
175
|
+
|
|
176
|
+
## Validation errors
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
Whatsapp::Messages.send_template!(to: "+15551234567", name: "x", language: { code: "klingon" })
|
|
180
|
+
# => ActiveModel::ValidationError: Code is not a valid WhatsApp language code.
|
|
181
|
+
# Supported codes: af, sq, ar, az, bn, bg, ca, zh_CN, ...
|
|
182
|
+
|
|
183
|
+
Whatsapp::Messages.send_template!(to: "+15551234567", name: "", language: { code: "en_US" })
|
|
184
|
+
# => ActiveModel::ValidationError: Name can't be blank
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`name` is checked for presence only — an unapproved or misspelled template name comes
|
|
188
|
+
back from Meta, not from the validator.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/template-messages>
|
|
193
|
+
· [supported languages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Text Messages
|
|
2
|
+
|
|
3
|
+
The default way to carry a conversational reply or notice. Most other kinds exist
|
|
4
|
+
because they need to express something plain text can't — media, buttons, structured
|
|
5
|
+
data.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello from ruby-whatsapp!")
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Fields
|
|
12
|
+
|
|
13
|
+
| Field | Required | Rules |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
16
|
+
| `body` | yes | Max **4096** characters |
|
|
17
|
+
| `preview_url` | no | `true` or `false`. **Defaults to `true`** |
|
|
18
|
+
|
|
19
|
+
`preview_url` controls whether WhatsApp renders a link preview card for the first URL
|
|
20
|
+
in the body. Turn it off for transactional messages where a preview would be noise:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
Whatsapp::Messages.send_text!(
|
|
24
|
+
to: "+15551234567",
|
|
25
|
+
body: "Track your order: https://example.com/orders/1234",
|
|
26
|
+
preview_url: false
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Serialized payload
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Whatsapp::Messages::Text.new(to: "+15551234567", body: "Hello from ruby-whatsapp!").serialize
|
|
34
|
+
# => {
|
|
35
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "text",
|
|
36
|
+
# text: { body: "Hello from ruby-whatsapp!", preview_url: true }
|
|
37
|
+
# }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `text` hash is deliberately **not** compacted, so `preview_url` is always present
|
|
41
|
+
on the wire even when it holds its default.
|
|
42
|
+
|
|
43
|
+
## Validation errors
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
Whatsapp::Messages.send_text!(to: "+15551234567", body: "")
|
|
47
|
+
# => ActiveModel::ValidationError: Body can't be blank
|
|
48
|
+
|
|
49
|
+
Whatsapp::Messages.send_text!(to: "+15551234567", body: "x" * 4097)
|
|
50
|
+
# => ActiveModel::ValidationError: Body is too long (maximum is 4096 characters)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/text-messages>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Video Messages
|
|
2
|
+
|
|
3
|
+
Shares a video — an instructional clip, a product demo, an unboxing.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Whatsapp::Messages.send_video!(to: "+15551234567", id: "1234567890", caption: "Demo video")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Fields
|
|
10
|
+
|
|
11
|
+
| Field | Required | Rules |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
14
|
+
| `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
|
|
15
|
+
| `link` | one of | A publicly reachable HTTPS URL |
|
|
16
|
+
| `caption` | no | Max **1024** characters |
|
|
17
|
+
|
|
18
|
+
**Either `id` or `link` is required.** Videos are the kind where uploading once and
|
|
19
|
+
reusing the `id` pays off most — a `link` makes Meta re-download the whole file on
|
|
20
|
+
every single send.
|
|
21
|
+
|
|
22
|
+
## Serialized payload
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
Whatsapp::Messages::Video.new(to: "+15551234567", id: "1234567890", caption: "Demo video").serialize
|
|
26
|
+
# => {
|
|
27
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "video",
|
|
28
|
+
# video: { id: "1234567890", caption: "Demo video" }
|
|
29
|
+
# }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Validation errors
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Whatsapp::Messages.send_video!(to: "+15551234567")
|
|
36
|
+
# => ActiveModel::ValidationError: Either id or link must be present
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/video-messages>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Scheduling Campaigns
|
|
2
|
+
|
|
3
|
+
A **campaign schedule** sends an approved [template](../message_templates/README.md) to an
|
|
4
|
+
audience at a time you choose. `Whatsapp::Schedule` wraps the `schedules` edge, which
|
|
5
|
+
lists an account's schedules and creates new ones.
|
|
6
|
+
|
|
7
|
+
> **Schedules send templates; they don't create them.** The template behind `hsm_id`
|
|
8
|
+
> must already exist and be approved. Create and manage it with
|
|
9
|
+
> [`Whatsapp::MessageTemplates`](../message_templates/README.md).
|
|
10
|
+
|
|
11
|
+
Addresses your **WhatsApp Business Account** (`waba_id`, not `phone_id`), the same as
|
|
12
|
+
[template management](../message_templates/README.md) and
|
|
13
|
+
[subscribed apps](../subscribed_app/README.md).
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
Whatsapp.configure do |config|
|
|
17
|
+
config.api_key = ENV.fetch("WHATSAPP_API_KEY")
|
|
18
|
+
config.waba_id = ENV.fetch("WHATSAPP_WABA_ID")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Whatsapp::Schedule::Create.call(
|
|
22
|
+
hsm_id: "1234567890", audience_id: "2345678901", waba_cs_id: "3456789012",
|
|
23
|
+
name: "Black Friday", description: "Black Friday promo to VIP customers",
|
|
24
|
+
delivery_time: Time.now.to_i + 3600
|
|
25
|
+
).id # => "9876543210"
|
|
26
|
+
|
|
27
|
+
Whatsapp::Schedule::List.call.map(&:name) # => ["Black Friday"]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Method | Request | Returns |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `List.call(client:, fields:, limit:, after:, before:)` | `GET /{waba_id}/schedules` | `Response::Collection` |
|
|
33
|
+
| `Create.call(hsm_id:, audience_id:, waba_cs_id:, name:, description:, delivery_time:, client:)` | `POST /{waba_id}/schedules` | `Response::Created` |
|
|
34
|
+
|
|
35
|
+
## Creating
|
|
36
|
+
|
|
37
|
+
Every field is required:
|
|
38
|
+
|
|
39
|
+
| Field | Type | Meaning |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `hsm_id` | String | The approved template to send |
|
|
42
|
+
| `audience_id` | String | The audience to send it to |
|
|
43
|
+
| `waba_cs_id` | String | Meta: "WABA customer status ID (phone number)" |
|
|
44
|
+
| `name` | String | The schedule's name |
|
|
45
|
+
| `description` | String | The schedule's description |
|
|
46
|
+
| `delivery_time` | Integer | When to send, as a Unix timestamp |
|
|
47
|
+
|
|
48
|
+
The examples below share this hash:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
attrs = {
|
|
52
|
+
hsm_id: "1234567890", audience_id: "2345678901", waba_cs_id: "3456789012",
|
|
53
|
+
name: "Black Friday", description: "Black Friday promo to VIP customers",
|
|
54
|
+
delivery_time: 1_764_000_000,
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`delivery_time` must be an integer. For a `Time`, pass `time.to_i`:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
Whatsapp::Schedule::Create.call(**attrs, delivery_time: Time.utc(2026, 11, 27, 9).to_i)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
All six are checked before the request, so a missing one costs nothing:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
Whatsapp::Schedule::Create.call(**attrs, name: "")
|
|
68
|
+
# => ActiveModel::ValidationError: Validation failed: Name can't be blank
|
|
69
|
+
|
|
70
|
+
Whatsapp::Schedule::Create.call(**attrs, delivery_time: 1_764_000_000.5)
|
|
71
|
+
# => ActiveModel::ValidationError: Validation failed: Delivery time must be an integer
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Nothing beyond that is checked. Meta publishes no length limits and no rule that
|
|
75
|
+
`delivery_time` must be in the future; anything it rejects comes back as a
|
|
76
|
+
`Whatsapp::Schedule::Error`.
|
|
77
|
+
|
|
78
|
+
> `waba_cs_id` is passed through exactly as given. Meta's description doesn't say
|
|
79
|
+
> whether it means a phone number ID or the number itself.
|
|
80
|
+
|
|
81
|
+
To inspect the payload without sending it, build the object yourself:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Whatsapp::Schedule::Create.new(**attrs).serialize
|
|
85
|
+
# => { hsm_id: "1234567890", audience_id: "2345678901", waba_cs_id: "3456789012",
|
|
86
|
+
# name: "Black Friday", description: "Black Friday promo to VIP customers",
|
|
87
|
+
# delivery_time: 1764000000 }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Listing
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
schedules = Whatsapp::Schedule::List.call
|
|
94
|
+
|
|
95
|
+
schedules.map(&:name) # Collection is Enumerable
|
|
96
|
+
schedules.first.delivery_time # => 1764000000
|
|
97
|
+
schedules.first.status # => "SCHEDULED"
|
|
98
|
+
schedules.first.id # => nil (see below)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Ask for `id` explicitly
|
|
102
|
+
|
|
103
|
+
**Meta's default fields leave out `id`.** The defaults are `name`, `description`,
|
|
104
|
+
`delivery_time`, and `status`. Request `id` with `fields:`, either as an Array or a
|
|
105
|
+
comma-joined String:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Whatsapp::Schedule::List.call(fields: Whatsapp::Schedule::List::Fields::ALL)
|
|
109
|
+
Whatsapp::Schedule::List.call(fields: %w[id name status])
|
|
110
|
+
Whatsapp::Schedule::List.call(fields: "id,name,status")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`List::Fields::ALL` is a convenience, not a whitelist. A field Meta adds later works
|
|
114
|
+
without a gem release.
|
|
115
|
+
|
|
116
|
+
### Paging
|
|
117
|
+
|
|
118
|
+
`limit:` sets the page size (Meta accepts 1–100). `after:` and `before:` take cursors.
|
|
119
|
+
Paging is exposed rather than followed automatically, so fetching every page is your
|
|
120
|
+
choice:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
page = Whatsapp::Schedule::List.call(limit: 50)
|
|
124
|
+
|
|
125
|
+
while page.next_cursor
|
|
126
|
+
page = Whatsapp::Schedule::List.call(limit: 50, after: page.next_cursor)
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`Collection#paging` exposes both cursors (`paging.before`, `paging.after`). It is the
|
|
131
|
+
same Graph API paging object [template listing](../message_templates/README.md) returns.
|
|
132
|
+
|
|
133
|
+
### Statuses
|
|
134
|
+
|
|
135
|
+
| Status | Predicate |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `SCHEDULED` | `scheduled?` |
|
|
138
|
+
| `SENDING` | `sending?` |
|
|
139
|
+
| `COMPLETED` | `completed?` |
|
|
140
|
+
| `FAILED` | `failed?` |
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
Whatsapp::Schedule::List.call.select(&:failed?).map(&:name)
|
|
144
|
+
schedule.status == Whatsapp::Schedule::Statuses::SCHEDULED
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`status` is the raw string Meta sent and is never validated, so a status Meta adds later
|
|
148
|
+
still comes through. Every predicate just answers `false` for it.
|
|
149
|
+
|
|
150
|
+
## Response shapes
|
|
151
|
+
|
|
152
|
+
| Class | Holds | From |
|
|
153
|
+
| --- | --- | --- |
|
|
154
|
+
| `Response::Collection` | `data: [Node]`, `paging`, `next_cursor`, `Enumerable` | `List` |
|
|
155
|
+
| `Response::Node` | `id`, `name`, `description`, `delivery_time`, `status` | each element of `List` |
|
|
156
|
+
| `Response::Created` | `id` | `Create` |
|
|
157
|
+
|
|
158
|
+
Every attribute tolerates being absent, since Meta returns only the fields you asked for.
|
|
159
|
+
|
|
160
|
+
## Errors
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
Whatsapp::Schedule::List.call
|
|
164
|
+
# => Whatsapp::Schedule::Error: waba_id is required for schedules;
|
|
165
|
+
# set it via Whatsapp.configure or Client.new(waba_id:)
|
|
166
|
+
|
|
167
|
+
# non-2xx response
|
|
168
|
+
# => Whatsapp::Schedule::Error: Failed to create schedule: 403 Forbidden - {...}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Every API failure raises `Whatsapp::Schedule::Error`, never the generic
|
|
172
|
+
`Whatsapp::RequestError`. Local validation raises `ActiveModel::ValidationError` before
|
|
173
|
+
any request. See [../errors.md](../errors.md).
|
|
174
|
+
|
|
175
|
+
## Targeting a different account
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
Whatsapp::Schedule::List.call(client: Whatsapp::Client.new(waba_id: "OTHER_WABA_ID"))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/schedules-api>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Managing Subscribed Apps
|
|
2
|
+
|
|
3
|
+
Before your app receives any [webhook](../webhooks/README.md) notifications for a
|
|
4
|
+
WhatsApp Business Account, it has to be **subscribed** to it.
|
|
5
|
+
`Whatsapp::SubscribedApp` wraps the `subscribed_apps` edge — the switch that turns
|
|
6
|
+
webhook delivery on or off.
|
|
7
|
+
|
|
8
|
+
> **Two halves of one thing.** This module makes Meta start sending;
|
|
9
|
+
> [`Whatsapp::Webhook`](../webhooks/README.md) deserializes what arrives. Neither is
|
|
10
|
+
> useful without the other.
|
|
11
|
+
|
|
12
|
+
Addresses your **WhatsApp Business Account** (`waba_id`, not `phone_id`) and needs the
|
|
13
|
+
`whatsapp_business_management` permission — same as
|
|
14
|
+
[template management](../message_templates/README.md).
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
Whatsapp.configure do |config|
|
|
18
|
+
config.api_key = ENV.fetch("WHATSAPP_API_KEY")
|
|
19
|
+
config.waba_id = ENV.fetch("WHATSAPP_WABA_ID")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Whatsapp::SubscribedApp::Subscribe.call # start webhook delivery
|
|
23
|
+
Whatsapp::SubscribedApp::List.call.map(&:name) # => ["My App"]
|
|
24
|
+
Whatsapp::SubscribedApp::Unsubscribe.call # stop it
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Method | Request | Returns |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `List.call(client:, fields:)` | `GET /{waba_id}/subscribed_apps` | `Response::Collection` |
|
|
30
|
+
| `Subscribe.call(client:, override_callback_uri:, verify_token:)` | `POST /{waba_id}/subscribed_apps` | `Response::Subscription` |
|
|
31
|
+
| `Unsubscribe.call(client:)` | `DELETE /{waba_id}/subscribed_apps` | `Response::Unsubscription` |
|
|
32
|
+
|
|
33
|
+
Three independent actions, so three classes rather than one class with three methods —
|
|
34
|
+
unlike [templates](../message_templates/README.md), subscribing and listing share no
|
|
35
|
+
identity or validation rules that would justify combining them.
|
|
36
|
+
|
|
37
|
+
## Subscribing
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
result = Whatsapp::SubscribedApp::Subscribe.call
|
|
41
|
+
|
|
42
|
+
result.success # => true
|
|
43
|
+
result.map(&:name) # => ["My App"] — every app now subscribed, Meta's own echo
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`Response::Subscription` is `Enumerable` over the same `Response::App` objects `List`
|
|
47
|
+
returns, so you can check the result without a second request.
|
|
48
|
+
|
|
49
|
+
### Per-WABA callback overrides
|
|
50
|
+
|
|
51
|
+
Tech Providers routing several WABAs' notifications to different callback URLs pass an
|
|
52
|
+
override instead of relying on the single callback URL configured on the app itself:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
Whatsapp::SubscribedApp::Subscribe.call(
|
|
56
|
+
override_callback_uri: "https://example.com/webhooks/acme_corp",
|
|
57
|
+
verify_token: "a-per-account-secret"
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Both are optional and compacted out of the request body when absent. The
|
|
62
|
+
`verify_token` you pass here is the one Meta will send in the
|
|
63
|
+
[GET handshake](../webhooks/README.md#verification-the-get-handshake) for that WABA —
|
|
64
|
+
pass the matching value to `Verification.call(verify_token:)`.
|
|
65
|
+
|
|
66
|
+
> No client-side format check is applied to `override_callback_uri`. Meta enforces it
|
|
67
|
+
> server-side, and this gem validates only what Meta documents as a client-side rule.
|
|
68
|
+
|
|
69
|
+
## Listing
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
apps = Whatsapp::SubscribedApp::List.call
|
|
73
|
+
|
|
74
|
+
apps.map(&:name) # Collection is Enumerable
|
|
75
|
+
apps.first.id # => "123456789"
|
|
76
|
+
apps.first.link # => "https://www.facebook.com/games/?app_id=..."
|
|
77
|
+
apps.first.override_callback_uri # => nil, or the per-WABA override
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Restrict what comes back with `fields:` — an Array or a String, comma-joined for you:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
Whatsapp::SubscribedApp::List.call(fields: %w[id name])
|
|
84
|
+
Whatsapp::SubscribedApp::List.call(fields: "id,name")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`Response::App` flattens Meta's `whatsapp_business_api_data` wrapper, so `id`, `name`,
|
|
88
|
+
and `link` sit directly on the object rather than a nested hash.
|
|
89
|
+
|
|
90
|
+
> No pagination — Meta documents none for this edge, and an account typically has very
|
|
91
|
+
> few subscribed apps.
|
|
92
|
+
|
|
93
|
+
## Unsubscribing
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
Whatsapp::SubscribedApp::Unsubscribe.call.success # => true
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Stops **all** webhook deliveries for this WABA immediately.
|
|
100
|
+
|
|
101
|
+
## Response shapes
|
|
102
|
+
|
|
103
|
+
Three actions, three genuinely different shapes — hence three classes:
|
|
104
|
+
|
|
105
|
+
| Class | Holds | From |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| `Response::Collection` | `data: [App]`, `Enumerable` | `List` |
|
|
108
|
+
| `Response::Subscription` | `success`, `data: [App]`, `Enumerable` | `Subscribe` |
|
|
109
|
+
| `Response::Unsubscription` | `success` | `Unsubscribe` |
|
|
110
|
+
|
|
111
|
+
`Response::App` — `id`, `name`, `link`, `override_callback_uri` — is composed by both
|
|
112
|
+
`Collection` and `Subscription` rather than duplicated.
|
|
113
|
+
|
|
114
|
+
`success` is a strict `response["success"] == true`, so it is never a truthy string
|
|
115
|
+
you have to second-guess.
|
|
116
|
+
|
|
117
|
+
## Errors
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
Whatsapp::SubscribedApp::Subscribe.call
|
|
121
|
+
# => Whatsapp::SubscribedApp::Error: waba_id is required for subscribed apps;
|
|
122
|
+
# set it via Whatsapp.configure or Client.new(waba_id:)
|
|
123
|
+
|
|
124
|
+
# non-2xx response
|
|
125
|
+
# => Whatsapp::SubscribedApp::Error: Failed to subscribe app: 400 Bad Request - {...}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Every action raises `Whatsapp::SubscribedApp::Error` — never the generic
|
|
129
|
+
`Whatsapp::RequestError`. See [../errors.md](../errors.md).
|
|
130
|
+
|
|
131
|
+
## Targeting a different account
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
Whatsapp::SubscribedApp::List.call(client: Whatsapp::Client.new(waba_id: "OTHER_WABA_ID"))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/reference/whatsapp-business-account/subscribed-apps-api>
|