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,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,212 @@
|
|
|
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 the
|
|
65
|
+
recipient and wraps the kind-specific hash in a common envelope:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
{
|
|
69
|
+
messaging_product: "whatsapp",
|
|
70
|
+
recipient_type: "individual",
|
|
71
|
+
to: "+15551234567",
|
|
72
|
+
type: "<kind>",
|
|
73
|
+
"<kind>": { ... } # the kind-specific payload
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Addressing: `to`, `recipient`, or both
|
|
78
|
+
|
|
79
|
+
There are two ways to name a recipient, and **at least one is required**:
|
|
80
|
+
|
|
81
|
+
| Keyword | Value |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `to` | The recipient's phone number |
|
|
84
|
+
| `recipient` | Their [business-scoped user ID](#business-scoped-user-ids-bsuid) (BSUID), or parent BSUID |
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi") # phone number
|
|
88
|
+
Whatsapp::Messages.send_text!(recipient: "US.13491208655302741918", body: "Hi") # BSUID
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pass both and Meta gives `to` precedence, answering as though `recipient` had not been
|
|
92
|
+
sent. Pass neither and you get an `ActiveModel::ValidationError` before any request:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Whatsapp::Messages.send_text!(body: "Hi")
|
|
96
|
+
# => ActiveModel::ValidationError: at least one of to or recipient must be provided
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Whichever you omit is left out of the payload entirely, never sent as `null`.
|
|
100
|
+
|
|
101
|
+
#### Business-scoped user IDs (BSUID)
|
|
102
|
+
|
|
103
|
+
Once a WhatsApp user adopts a username, their phone number can stop appearing in your
|
|
104
|
+
webhooks — and their BSUID becomes the only identifier you have. `recipient` is how you
|
|
105
|
+
reply to them. A BSUID is a country code, a period, and up to 128 alphanumeric
|
|
106
|
+
characters (`"US.13491208655302741918"`); a
|
|
107
|
+
[parent BSUID](https://developers.facebook.com/documentation/business-messaging/whatsapp/business-scoped-user-ids#parent-business-scoped-user-ids),
|
|
108
|
+
usable across enrolled business portfolios, carries an extra `ENT.` segment
|
|
109
|
+
(`"US.ENT.11815799212886844830"`). Pass the whole value — dropping or altering any part
|
|
110
|
+
of it fails the request.
|
|
111
|
+
|
|
112
|
+
The gem does **not** format-check it. Meta describes the shape but publishes no
|
|
113
|
+
client-side rule, and the value you pass here normally comes straight off the
|
|
114
|
+
[inbound webhook side](../webhooks/messages.md#business-scoped-user-ids-bsuid), which
|
|
115
|
+
keeps it as a raw string. Replying to a customer Meta just told you about is exactly the
|
|
116
|
+
case a local regex would break.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# In your webhook handler, replying to whoever wrote in
|
|
120
|
+
contact = change.value.contacts.first
|
|
121
|
+
Whatsapp::Messages.send_text!(recipient: contact.user_id, body: "Thanks for reaching out!")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
BSUIDs are scoped to a business portfolio: a BSUID from one portfolio cannot be
|
|
125
|
+
messaged from a phone number owned by another.
|
|
126
|
+
|
|
127
|
+
Three kinds break the `type == kind` symmetry because Meta transports them as
|
|
128
|
+
interactive messages: [`address`](address.md), [`location_request`](location_request.md),
|
|
129
|
+
and of course [`interactive`](interactive.md) all serialize with `type: "interactive"`.
|
|
130
|
+
|
|
131
|
+
## The response
|
|
132
|
+
|
|
133
|
+
A successful `send!` returns a `Whatsapp::Messages::Response`, deserialized from a
|
|
134
|
+
body shaped like:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"messaging_product": "whatsapp",
|
|
139
|
+
"contacts": [{ "input": "+15551234567", "wa_id": "15551234567" }],
|
|
140
|
+
"messages": [{ "id": "wamid.HBgLMTU1NTU1NTU1NTUV..." }]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
response = Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi")
|
|
146
|
+
|
|
147
|
+
response.messaging_product # => "whatsapp"
|
|
148
|
+
response.messages.first.id # => "wamid.HBgLMTU1NTU1NTU1NTUV..."
|
|
149
|
+
response.contacts.first.input # => "+15551234567"
|
|
150
|
+
response.contacts.first.wa_id # => "15551234567"
|
|
151
|
+
response.contacts.first.user_id # => nil — see below
|
|
152
|
+
response.success # => nil for a normal send
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Which identifiers come back mirrors how you addressed the message:
|
|
156
|
+
|
|
157
|
+
| Accessor | Value |
|
|
158
|
+
| --- | --- |
|
|
159
|
+
| `input` | Exactly what you sent: the phone number, the BSUID or parent BSUID, or a group ID |
|
|
160
|
+
| `wa_id` | The phone number — **only** if you addressed it by phone number |
|
|
161
|
+
| `user_id` | The BSUID or parent BSUID — **only** if you addressed it by BSUID |
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
response = Whatsapp::Messages.send_text!(recipient: "US.13491208655302741918", body: "Hi")
|
|
165
|
+
|
|
166
|
+
response.contacts.first.input # => "US.13491208655302741918"
|
|
167
|
+
response.contacts.first.user_id # => "US.13491208655302741918"
|
|
168
|
+
response.contacts.first.wa_id # => nil
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
> **The precedence trap.** Send both `to` and `recipient` and `user_id` comes back
|
|
172
|
+
> **omitted**, because `to` wins and Meta answers as though you had only sent that. So
|
|
173
|
+
> `user_id` being `nil` does not mean the recipient has no BSUID — and code that
|
|
174
|
+
> correlates a send by reading `contacts.first.user_id` must not also pass `to:`.
|
|
175
|
+
|
|
176
|
+
Unlike the webhook classes, which separate `user_id` from `parent_user_id`, the send
|
|
177
|
+
response returns either kind in the single `user_id` field.
|
|
178
|
+
|
|
179
|
+
`#success` is `nil` for a normal message send and `true`/`false` only for the
|
|
180
|
+
status-update endpoints that reply `{"success": true}` — today just
|
|
181
|
+
[`mark_message_as_read!`](mark_message_as_read.md).
|
|
182
|
+
|
|
183
|
+
## Errors
|
|
184
|
+
|
|
185
|
+
| Raised | When |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `ArgumentError` | A required keyword is missing (the recipient is a *validation*, not an arity, concern — see below) |
|
|
188
|
+
| `ActiveModel::ValidationError` | A field fails a local validation |
|
|
189
|
+
| `Whatsapp::Messages::PayloadError` | `kind:` is unrecognized — only reachable through the `Messages.new(kind:, payload:)` form |
|
|
190
|
+
| `Whatsapp::RequestError` | The API answered with a non-2xx status |
|
|
191
|
+
|
|
192
|
+
Because every kind runs `validate!` at the end of its own `initialize`, an invalid
|
|
193
|
+
field raises at *construction* time, before a client is ever touched:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
Whatsapp::Messages.send_image!(to: "+15551234567")
|
|
197
|
+
# => ActiveModel::ValidationError: Either id or link must be present
|
|
198
|
+
|
|
199
|
+
Whatsapp::Messages.send_text!(body: "Hi")
|
|
200
|
+
# => ActiveModel::ValidationError: at least one of to or recipient must be provided
|
|
201
|
+
|
|
202
|
+
Whatsapp::Messages.new(kind: :telepathy, payload: { to: "+1" })
|
|
203
|
+
# => Whatsapp::Messages::PayloadError: Unknown message kind: :telepathy.
|
|
204
|
+
# Known kinds: text, image, audio, video, document, sticker, contacts,
|
|
205
|
+
# reaction, location, address, location_request, template, interactive
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
See [../errors.md](../errors.md) for the full hierarchy.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
**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` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
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` / `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
|
+
|
|
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>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Contact Messages
|
|
2
|
+
|
|
3
|
+
Shares a structured, vCard-like contact card the recipient can tap once to save,
|
|
4
|
+
instead of pasting a phone number into a text message and hoping they copy it
|
|
5
|
+
correctly.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Whatsapp::Messages.send_contacts!(
|
|
9
|
+
to: "+15551234567",
|
|
10
|
+
contacts: [{ name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" } }]
|
|
11
|
+
)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Fields
|
|
15
|
+
|
|
16
|
+
| Field | Required | Rules |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
19
|
+
| `contacts` | yes | An array containing **exactly one** contact |
|
|
20
|
+
|
|
21
|
+
> **Exactly one contact.** The Cloud API currently accepts a single contact per
|
|
22
|
+
> message. An empty array raises `Contacts can't be blank`; two or more raises
|
|
23
|
+
> `Contacts is too long`. To share several people, send several messages.
|
|
24
|
+
|
|
25
|
+
Each entry in `contacts` is a hash (or a `Contacts::Contact`) composed of:
|
|
26
|
+
|
|
27
|
+
| Key | Required | Type |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `name` | yes | `Name` |
|
|
30
|
+
| `phones` | no | `Array<Phone>` |
|
|
31
|
+
| `emails` | no | `Array<Email>` |
|
|
32
|
+
| `addresses` | no | `Array<Address>` |
|
|
33
|
+
| `urls` | no | `Array<Url>` |
|
|
34
|
+
| `org` | no | `Org` |
|
|
35
|
+
| `birthday` | no | `"YYYY-MM-DD"` string |
|
|
36
|
+
|
|
37
|
+
### Sub-object fields
|
|
38
|
+
|
|
39
|
+
| Object | Fields | Notes |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `Name` | `formatted_name` (**required**), `first_name`, `last_name`, `middle_name`, `prefix`, `suffix` | |
|
|
42
|
+
| `Phone` | `phone` (**required**), `type`, `wa_id` | `type`: `CELL` `MAIN` `IPHONE` `HOME` `WORK` `OTHER` |
|
|
43
|
+
| `Email` | `email` (**required**), `type` | `type`: `HOME` `WORK` `OTHER` |
|
|
44
|
+
| `Address` | `street`, `city`, `state`, `zip`, `country`, `country_code`, `type` | all optional; `type`: `HOME` `WORK` `OTHER` |
|
|
45
|
+
| `Url` | `url` (**required**), `type` | `type`: `WEBSITE` `HOMEPAGE` `WORK` `HOME` `OTHER` |
|
|
46
|
+
| `Org` | `company`, `department`, `title` | all optional, no validation |
|
|
47
|
+
|
|
48
|
+
## A full example
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
Whatsapp::Messages.send_contacts!(
|
|
52
|
+
to: "+15551234567",
|
|
53
|
+
contacts: [
|
|
54
|
+
{
|
|
55
|
+
name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" },
|
|
56
|
+
phones: [{ phone: "+15550001111", type: "WORK" }],
|
|
57
|
+
emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
58
|
+
addresses: [{ street: "1 Hacker Way", city: "Menlo Park", state: "CA",
|
|
59
|
+
zip: "94025", country: "United States", country_code: "us", type: "WORK" }],
|
|
60
|
+
urls: [{ url: "https://example.com", type: "WEBSITE" }],
|
|
61
|
+
org: { company: "Acme Inc.", department: "Support", title: "Lead" },
|
|
62
|
+
birthday: "1990-05-12",
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Serialized payload
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
Whatsapp::Messages::Contacts.new(
|
|
72
|
+
to: "+15551234567",
|
|
73
|
+
contacts: [
|
|
74
|
+
{
|
|
75
|
+
name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" },
|
|
76
|
+
phones: [{ phone: "+15550001111", type: "WORK" }],
|
|
77
|
+
emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
78
|
+
org: { company: "Acme Inc." },
|
|
79
|
+
birthday: "1990-05-12",
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
).serialize
|
|
83
|
+
# => {
|
|
84
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "contacts",
|
|
85
|
+
# contacts: [{
|
|
86
|
+
# name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" },
|
|
87
|
+
# phones: [{ phone: "+15550001111", type: "WORK" }],
|
|
88
|
+
# emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
89
|
+
# org: { company: "Acme Inc." },
|
|
90
|
+
# birthday: "1990-05-12"
|
|
91
|
+
# }]
|
|
92
|
+
# }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Every sub-object compacts its own hash, and `Contact` omits `phones`/`emails`/
|
|
96
|
+
`addresses`/`urls` entirely when they're empty — so a minimal card is just `name`.
|
|
97
|
+
|
|
98
|
+
> **Known gap.** `Contacts::Name` validates only the presence of `formatted_name`, but
|
|
99
|
+
> live testing returns `(#131009) ContactName should have atleast one optional value be
|
|
100
|
+
> set along with formatted Name`. Meta requires at least one *additional* name field
|
|
101
|
+
> (e.g. `first_name`) alongside `formatted_name`; this class doesn't yet enforce it, so
|
|
102
|
+
> a name-only card passes local validation and is rejected server-side. Always set a
|
|
103
|
+
> second name field.
|
|
104
|
+
|
|
105
|
+
## Validation errors
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [])
|
|
109
|
+
# => ActiveModel::ValidationError: Contacts can't be blank
|
|
110
|
+
|
|
111
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [contact_a, contact_b])
|
|
112
|
+
# => ActiveModel::ValidationError: Contacts is too long (maximum is 1 character)
|
|
113
|
+
|
|
114
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [{ name: { first_name: "Jane" } }])
|
|
115
|
+
# => ArgumentError: missing keyword: :formatted_name
|
|
116
|
+
|
|
117
|
+
Whatsapp::Messages.send_contacts!(to: "+15551234567", contacts: [{ name: { formatted_name: "" } }])
|
|
118
|
+
# => ActiveModel::ValidationError: Formatted name can't be blank
|
|
119
|
+
|
|
120
|
+
Whatsapp::Messages.send_contacts!(
|
|
121
|
+
to: "+15551234567",
|
|
122
|
+
contacts: [{ name: { formatted_name: "Jane Doe" }, phones: [{ phone: "+1", type: "MOBILE" }] }]
|
|
123
|
+
)
|
|
124
|
+
# => ActiveModel::ValidationError: Type is not included in the list
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/contacts-messages>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Document Messages
|
|
2
|
+
|
|
3
|
+
Shares a file — an invoice, a contract, a PDF — with an optional caption and the
|
|
4
|
+
filename the recipient sees.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
Whatsapp::Messages.send_document!(
|
|
8
|
+
to: "+15551234567",
|
|
9
|
+
link: "https://example.com/invoice.pdf",
|
|
10
|
+
filename: "invoice.pdf"
|
|
11
|
+
)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Fields
|
|
15
|
+
|
|
16
|
+
| Field | Required | Rules |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `to` / `recipient` | at least one | Phone number and/or [BSUID](README.md#addressing-to-recipient-or-both) |
|
|
19
|
+
| `id` | one of | A media ID from [`Media#upload`](../media/README.md) |
|
|
20
|
+
| `link` | one of | A publicly reachable HTTPS URL |
|
|
21
|
+
| `caption` | no | Max **1024** characters |
|
|
22
|
+
| `filename` | no | The name shown to the recipient; not validated |
|
|
23
|
+
|
|
24
|
+
**Either `id` or `link` is required.** Without `filename`, WhatsApp falls back to
|
|
25
|
+
whatever it can infer from the URL or media asset, which is usually an unhelpful
|
|
26
|
+
hash — set it whenever the recipient will save the file.
|
|
27
|
+
|
|
28
|
+
## Serialized payload
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
Whatsapp::Messages::Document.new(
|
|
32
|
+
to: "+15551234567", link: "https://example.com/invoice.pdf", filename: "invoice.pdf"
|
|
33
|
+
).serialize
|
|
34
|
+
# => {
|
|
35
|
+
# messaging_product: "whatsapp", recipient_type: "individual", to: "+15551234567", type: "document",
|
|
36
|
+
# document: { link: "https://example.com/invoice.pdf", filename: "invoice.pdf" }
|
|
37
|
+
# }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Validation errors
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
Whatsapp::Messages.send_document!(to: "+15551234567", filename: "invoice.pdf")
|
|
44
|
+
# => ActiveModel::ValidationError: Either id or link must be present
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/document-messages>
|