ruby-whatsapp 0.3.1 → 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 +14 -1
- data/README.md +292 -497
- 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/deregister.rb +38 -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/register.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/request_code.rb +85 -0
- data/lib/ruby/whatsapp/business_phone_number/response.rb +39 -0
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +27 -0
- data/lib/ruby/whatsapp/business_phone_number/verify_code.rb +63 -0
- data/lib/ruby/whatsapp/business_phone_number.rb +29 -0
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/subscribed_app/list.rb +28 -0
- data/lib/ruby/whatsapp/subscribed_app/response/app.rb +57 -0
- data/lib/ruby/whatsapp/subscribed_app/response/collection.rb +41 -0
- data/lib/ruby/whatsapp/subscribed_app/response/subscription.rb +49 -0
- data/lib/ruby/whatsapp/subscribed_app/response/unsubscription.rb +31 -0
- data/lib/ruby/whatsapp/subscribed_app/response.rb +19 -0
- data/lib/ruby/whatsapp/subscribed_app/subscribe.rb +33 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +24 -0
- data/lib/ruby/whatsapp/subscribed_app/unsubscribe.rb +26 -0
- data/lib/ruby/whatsapp/subscribed_app.rb +15 -0
- data/lib/ruby/whatsapp/version.rb +1 -1
- metadata +80 -1
data/README.md
CHANGED
|
@@ -1,643 +1,438 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">ruby-whatsapp</h1>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<strong>A small, dependency-light Ruby client for the Meta WhatsApp Cloud API.</strong>
|
|
5
|
+
</p>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://rubygems.org/gems/ruby-whatsapp"><img alt="Gem Version" src="https://img.shields.io/gem/v/ruby-whatsapp.svg"></a>
|
|
9
|
+
<a href="https://github.com/saleszera/ruby-whatsapp/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/saleszera/ruby-whatsapp/actions/workflows/main.yml/badge.svg"></a>
|
|
10
|
+
<a href="https://rubygems.org/gems/ruby-whatsapp"><img alt="Downloads" src="https://img.shields.io/gem/dt/ruby-whatsapp.svg"></a>
|
|
11
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg"></a>
|
|
12
|
+
<a href="https://www.ruby-lang.org"><img alt="Ruby" src="https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D.svg"></a>
|
|
13
|
+
</p>
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
---
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
- [Image](#image)
|
|
18
|
-
- [Video](#video)
|
|
19
|
-
- [Audio](#audio)
|
|
20
|
-
- [Document](#document)
|
|
21
|
-
- [Sticker](#sticker)
|
|
22
|
-
- [Reaction](#reaction)
|
|
23
|
-
- [Location](#location)
|
|
24
|
-
- [Contacts](#contacts)
|
|
25
|
-
- [Address](#address)
|
|
26
|
-
- [Location Request](#location-request)
|
|
27
|
-
- [Template](#template)
|
|
28
|
-
- [Interactive](#interactive)
|
|
29
|
-
- [Mark Message As Read](#mark-message-as-read)
|
|
30
|
-
- [Handling Responses](#handling-responses)
|
|
31
|
-
- [Media](#media)
|
|
32
|
-
- [Managing Templates](#managing-templates)
|
|
33
|
-
- [Webhooks](#webhooks)
|
|
34
|
-
- [Development](#development)
|
|
35
|
-
- [Contributing](#contributing)
|
|
36
|
-
- [License](#license)
|
|
37
|
-
|
|
38
|
-
## Installation
|
|
39
|
-
|
|
40
|
-
Add to your Gemfile:
|
|
17
|
+
Every message type the Cloud API supports — text, media, location, contacts,
|
|
18
|
+
templates, and the full family of interactive messages — is modeled as its own
|
|
19
|
+
`ActiveModel`-validated Ruby class. A malformed payload raises **in your own process,
|
|
20
|
+
naming the field that's wrong**, instead of travelling to Meta and coming back as
|
|
21
|
+
error `131009` — the catch-all code the Cloud API returns for a too-long caption, an
|
|
22
|
+
unsupported message type, a bad message ID, and a dozen other unrelated mistakes.
|
|
41
23
|
|
|
42
24
|
```ruby
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Then run `bundle install`. Or install directly:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
gem install ruby-whatsapp
|
|
50
|
-
```
|
|
25
|
+
photo = "https://example.com/new-arrivals.jpg"
|
|
51
26
|
|
|
52
|
-
|
|
27
|
+
# Send it 📸
|
|
28
|
+
Whatsapp::Messages.send_image!(to: "+15551234567", link: photo, caption: "Just landed")
|
|
53
29
|
|
|
54
|
-
|
|
55
|
-
Whatsapp.
|
|
56
|
-
|
|
57
|
-
config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID") # the sending phone number ID
|
|
58
|
-
# optional overrides:
|
|
59
|
-
# config.version = "v24.0"
|
|
60
|
-
# config.host = "https://graph.facebook.com"
|
|
61
|
-
# config.waba_id = ENV.fetch("WHATSAPP_WABA_ID") # for template management, see below
|
|
62
|
-
# config.verify_token = ENV.fetch("WHATSAPP_VERIFY_TOKEN") # for webhooks, see below
|
|
63
|
-
# config.app_secret = ENV.fetch("WHATSAPP_APP_SECRET") # for webhooks, see below
|
|
64
|
-
end
|
|
30
|
+
# Get a field wrong, and you find out here — not from Meta, three seconds later
|
|
31
|
+
Whatsapp::Messages.send_image!(to: "+15551234567", link: photo, caption: "x" * 2000)
|
|
32
|
+
# => ActiveModel::ValidationError: Caption is too long (maximum is 1024 characters)
|
|
65
33
|
```
|
|
66
34
|
|
|
67
|
-
|
|
35
|
+
Beyond sending, the gem covers the whole surface: media upload and download, template
|
|
36
|
+
creation and management, inbound webhook parsing, webhook subscription, phone number
|
|
37
|
+
onboarding, and reading or updating the business account itself.
|
|
68
38
|
|
|
69
|
-
##
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
require "ruby/whatsapp"
|
|
73
|
-
|
|
74
|
-
Whatsapp.configure do |config|
|
|
75
|
-
config.api_key = ENV.fetch("WHATSAPP_TOKEN")
|
|
76
|
-
config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
response = Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello from ruby-whatsapp!")
|
|
80
|
-
|
|
81
|
-
response.messages.first.id # => "wamid.HBgLMTU1NTU1NTU1NTUV..."
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Features
|
|
85
|
-
|
|
86
|
-
- **Typed, validated message classes** for every Cloud API message kind — invalid payloads raise before any HTTP request is made.
|
|
87
|
-
- **Persistent HTTP connections** via `HTTP.persistent`, reused across requests.
|
|
88
|
-
- **Pluggable instrumentation** — pass a `Logger` to `Whatsapp::Client.new` to log every request/response.
|
|
89
|
-
- **Hardened media downloads** — `Media#download` refuses to attach the bearer token to non-HTTPS URLs or hosts outside an allowlist, so a token can never leak to an attacker-influenced URL.
|
|
90
|
-
- **Structured response parsing** — `Whatsapp::Messages::Response` exposes typed `#contacts` and `#messages` instead of raw JSON.
|
|
91
|
-
- **Template management** — create, list, edit and delete the message templates on your WhatsApp Business Account from Ruby, with Meta's documented rules checked client-side so a rejection costs a validation error instead of a 24-hour review cycle.
|
|
92
|
-
- **Inbound webhook parsing** — a typed object tree for all 19 documented Meta notification field types, plus signature verification.
|
|
93
|
-
|
|
94
|
-
## Sending Messages
|
|
95
|
-
|
|
96
|
-
Every registered message kind gets its own `Whatsapp::Messages.send_<kind>!` class method — pass the recipient and the kind-specific fields as keyword arguments:
|
|
39
|
+
## Table of Contents
|
|
97
40
|
|
|
98
|
-
|
|
99
|
-
|
|
41
|
+
- [✨ Why ruby-whatsapp](#-why-ruby-whatsapp)
|
|
42
|
+
- [📦 Installation](#-installation)
|
|
43
|
+
- [🔧 Configuration](#-configuration)
|
|
44
|
+
- [🚀 Quick Start](#-quick-start)
|
|
45
|
+
- [📚 Documentation](#-documentation)
|
|
46
|
+
- [💬 Sending Messages](#-sending-messages)
|
|
47
|
+
- [📋 Managing Templates](#-managing-templates)
|
|
48
|
+
- [🔔 Webhooks](#-webhooks)
|
|
49
|
+
- [📷 Media](#-media)
|
|
50
|
+
- [🔌 Subscribed Apps](#-subscribed-apps)
|
|
51
|
+
- [📞 Business Phone Numbers](#-business-phone-numbers)
|
|
52
|
+
- [🚨 Errors](#-errors)
|
|
53
|
+
- [🧩 Compatibility](#-compatibility)
|
|
54
|
+
- [🔨 Development](#-development)
|
|
55
|
+
- [🤝 Contributing](#-contributing)
|
|
56
|
+
- [📄 License](#-license)
|
|
57
|
+
|
|
58
|
+
## ✨ Why ruby-whatsapp
|
|
59
|
+
|
|
60
|
+
- **Typed, validated message classes** for every Cloud API message kind — invalid
|
|
61
|
+
payloads raise before any HTTP request is made, with the failing attribute named.
|
|
62
|
+
- **Template rules checked client-side.** Meta's documented constraints — name format,
|
|
63
|
+
character limits, placeholder/example matching, quick-reply contiguity, carousel
|
|
64
|
+
structure — are enforced locally, so a mistake costs a validation error instead of a
|
|
65
|
+
24-hour review cycle.
|
|
66
|
+
- **Hardened media downloads.** `Media#download` refuses to attach your bearer token to
|
|
67
|
+
a non-HTTPS URL or a host outside an allowlist, so a token can never leak to an
|
|
68
|
+
attacker-influenced URL.
|
|
69
|
+
- **Secrets stay out of logs.** `api_key`, `app_secret`, and `verify_token` are
|
|
70
|
+
redacted from every `#inspect`, including credentials in transit like a
|
|
71
|
+
registration PIN.
|
|
72
|
+
- **Inbound webhooks, fully typed.** An object tree for all 19 documented Meta
|
|
73
|
+
notification fields, plus HMAC signature verification and a Rails controller
|
|
74
|
+
generator.
|
|
75
|
+
- **Persistent HTTP connections** via `HTTP.persistent`, reused across requests, with
|
|
76
|
+
pluggable logging.
|
|
77
|
+
- **Four runtime dependencies**, no Rails requirement. Rails integration activates
|
|
78
|
+
itself when Rails is present.
|
|
79
|
+
|
|
80
|
+
## 📦 Installation
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Gemfile
|
|
84
|
+
gem "ruby-whatsapp"
|
|
100
85
|
```
|
|
101
86
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
If the kind is only known at runtime, drop down to the underlying factory these methods are built on:
|
|
105
|
-
|
|
106
|
-
```ruby
|
|
107
|
-
Whatsapp::Messages.new(
|
|
108
|
-
kind: :text, # or a variable
|
|
109
|
-
payload: { to: "+15551234567", body: "Hello!" },
|
|
110
|
-
client: Whatsapp::Client.new # optional — defaults to a new Client built from Whatsapp.configuration
|
|
111
|
-
).send!
|
|
87
|
+
```bash
|
|
88
|
+
bundle install
|
|
112
89
|
```
|
|
113
90
|
|
|
114
|
-
|
|
91
|
+
Or standalone:
|
|
115
92
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
| [`send_text!`](#text) | Plain text with an optional link preview |
|
|
119
|
-
| [`send_image!`](#image) | An image with an optional caption |
|
|
120
|
-
| [`send_video!`](#video) | A video with an optional caption |
|
|
121
|
-
| [`send_audio!`](#audio) | An audio clip |
|
|
122
|
-
| [`send_document!`](#document) | A file with an optional caption and filename |
|
|
123
|
-
| [`send_sticker!`](#sticker) | A sticker |
|
|
124
|
-
| [`send_reaction!`](#reaction) | An emoji reaction to a previous message |
|
|
125
|
-
| [`send_location!`](#location) | A latitude/longitude pin |
|
|
126
|
-
| [`send_contacts!`](#contacts) | A rich contact card |
|
|
127
|
-
| [`send_address!`](#address) | A delivery-address request/confirmation form (India & Singapore only) |
|
|
128
|
-
| [`send_location_request!`](#location-request) | A prompt asking the user to share their location |
|
|
129
|
-
| [`send_template!`](#template) | A pre-approved marketing/utility/authentication template |
|
|
130
|
-
| [`send_interactive!`](#interactive) | Reply buttons, lists, CTA URLs, or carousels |
|
|
131
|
-
|
|
132
|
-
### Text
|
|
133
|
-
|
|
134
|
-
```ruby
|
|
135
|
-
Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello!", preview_url: false)
|
|
93
|
+
```bash
|
|
94
|
+
gem install ruby-whatsapp
|
|
136
95
|
```
|
|
137
96
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
### Image
|
|
97
|
+
## 🔧 Configuration
|
|
141
98
|
|
|
142
99
|
```ruby
|
|
143
|
-
Whatsapp
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
100
|
+
Whatsapp.configure do |config|
|
|
101
|
+
config.api_key = ENV.fetch("WHATSAPP_API_KEY") # a Meta system-user / app access token
|
|
102
|
+
config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID") # the sending phone number ID
|
|
103
|
+
end
|
|
148
104
|
```
|
|
149
105
|
|
|
150
|
-
|
|
106
|
+
| Option | Default | Needed for |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `api_key` | — | Everything |
|
|
109
|
+
| `phone_id` | — | Messages, media, phone-number onboarding |
|
|
110
|
+
| `waba_id` | — | Template management, subscribed apps, the business account |
|
|
111
|
+
| `verify_token` | — | The webhook GET handshake |
|
|
112
|
+
| `app_secret` | — | Webhook signature verification |
|
|
113
|
+
| `host` | `https://graph.facebook.com` | Overriding the API host |
|
|
114
|
+
| `version` | `v24.0` | Pinning a Graph API version |
|
|
115
|
+
| `media_host_allowlist` | 3 Meta hosts | Media download safety |
|
|
151
116
|
|
|
152
|
-
|
|
117
|
+
**Which ID addresses what** — the most common source of confusion:
|
|
153
118
|
|
|
154
|
-
```ruby
|
|
155
|
-
Whatsapp::Messages.send_video!(to: "+15551234567", id: "1234567890", caption: "Demo video")
|
|
156
119
|
```
|
|
120
|
+
phone_id ──► Messages · Media · BusinessPhoneNumber
|
|
121
|
+
permission: whatsapp_business_messaging
|
|
157
122
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
### Audio
|
|
161
|
-
|
|
162
|
-
```ruby
|
|
163
|
-
Whatsapp::Messages.send_audio!(to: "+15551234567", link: "https://example.com/clip.mp3")
|
|
123
|
+
waba_id ──► MessageTemplates · SubscribedApp · BusinessPhoneNumber::Account
|
|
124
|
+
permission: whatsapp_business_management
|
|
164
125
|
```
|
|
165
126
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
### Document
|
|
127
|
+
`BusinessPhoneNumber` appears on both sides: its onboarding actions address a phone
|
|
128
|
+
number, its `Account` actions the account that number belongs to.
|
|
169
129
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
to: "+15551234567",
|
|
173
|
-
link: "https://example.com/invoice.pdf",
|
|
174
|
-
filename: "invoice.pdf"
|
|
175
|
-
)
|
|
176
|
-
```
|
|
130
|
+
`api_key`, `app_secret`, and `verify_token` are redacted from `Configuration#inspect`
|
|
131
|
+
and `Client#inspect`, so they will not leak into logs or error reports.
|
|
177
132
|
|
|
178
|
-
|
|
133
|
+
→ **[Full configuration reference](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/configuration.md)**
|
|
179
134
|
|
|
180
|
-
|
|
135
|
+
## 🚀 Quick Start
|
|
181
136
|
|
|
182
137
|
```ruby
|
|
183
|
-
|
|
184
|
-
```
|
|
138
|
+
require "ruby/whatsapp"
|
|
185
139
|
|
|
186
|
-
|
|
140
|
+
Whatsapp.configure do |config|
|
|
141
|
+
config.api_key = ENV.fetch("WHATSAPP_API_KEY")
|
|
142
|
+
config.phone_id = ENV.fetch("WHATSAPP_PHONE_ID")
|
|
143
|
+
end
|
|
187
144
|
|
|
188
|
-
|
|
145
|
+
response = Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello from ruby-whatsapp!")
|
|
189
146
|
|
|
190
|
-
|
|
191
|
-
|
|
147
|
+
response.messages.first.id # => "wamid.HBgLMTU1NTU1NTU1NTUV..."
|
|
148
|
+
response.contacts.first.wa_id # => "15551234567"
|
|
192
149
|
```
|
|
193
150
|
|
|
194
|
-
|
|
151
|
+
## 📚 Documentation
|
|
195
152
|
|
|
196
|
-
|
|
153
|
+
| Area | What it covers |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| [**Configuration**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/configuration.md) | Credentials, the client, connection reuse, instrumentation |
|
|
156
|
+
| [**Messages**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/README.md) | Every message kind, one page each, with exact payloads |
|
|
157
|
+
| [**Message Templates**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/message_templates/README.md) | Creating and managing templates: standard, auth, carousel, offers, library |
|
|
158
|
+
| [**Webhooks**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/webhooks/README.md) | Install, verification, signatures, and all 19 notification fields |
|
|
159
|
+
| [**Media**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/media/README.md) | Upload, download, delete, and the token-safety allowlist |
|
|
160
|
+
| [**Subscribed Apps**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/subscribed_app/README.md) | Turning webhook delivery on and off for an account |
|
|
161
|
+
| [**Business Phone Numbers**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/README.md) | Onboarding: request code → verify → register |
|
|
162
|
+
| [**Business Account**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/account.md) | Reading and updating the account: name, timezone, review status |
|
|
163
|
+
| [**Business Profile**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/profile.md) | The profile a user sees: about, description, address, vertical, picture |
|
|
164
|
+
| [**Errors**](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/errors.md) | The exception hierarchy and retry strategy |
|
|
197
165
|
|
|
198
|
-
|
|
199
|
-
Whatsapp::Messages.send_location!(to: "+15551234567", latitude: 37.4847, longitude: -122.1477, name: "Meta HQ")
|
|
200
|
-
```
|
|
166
|
+
Or start at the [documentation index](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/README.md).
|
|
201
167
|
|
|
202
|
-
|
|
168
|
+
## 💬 Sending Messages
|
|
203
169
|
|
|
204
|
-
|
|
170
|
+
Every registered kind gets its own `Whatsapp::Messages.send_<kind>!` class method:
|
|
205
171
|
|
|
206
172
|
```ruby
|
|
207
|
-
Whatsapp::Messages.
|
|
173
|
+
Whatsapp::Messages.send_interactive!(
|
|
208
174
|
to: "+15551234567",
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
214
|
-
org: { company: "Acme Inc." },
|
|
215
|
-
birthday: "1990-05-12",
|
|
216
|
-
},
|
|
217
|
-
]
|
|
175
|
+
type: :reply_buttons,
|
|
176
|
+
body: "Would you like to confirm your order?",
|
|
177
|
+
action: { buttons: [{ id: "confirm", title: "Confirm" },
|
|
178
|
+
{ id: "cancel", title: "Cancel" }] }
|
|
218
179
|
)
|
|
219
180
|
```
|
|
220
181
|
|
|
221
|
-
|
|
182
|
+
| Method | Sends |
|
|
183
|
+
| --- | --- |
|
|
184
|
+
| [`send_text!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/text.md) | Plain text with an optional link preview |
|
|
185
|
+
| [`send_image!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/image.md) | An image with an optional caption |
|
|
186
|
+
| [`send_video!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/video.md) | A video with an optional caption |
|
|
187
|
+
| [`send_audio!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/audio.md) | A voice note or audio clip |
|
|
188
|
+
| [`send_document!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/document.md) | A file with an optional caption and filename |
|
|
189
|
+
| [`send_sticker!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/sticker.md) | A sticker |
|
|
190
|
+
| [`send_reaction!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/reaction.md) | An emoji reaction to a previous message |
|
|
191
|
+
| [`send_location!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/location.md) | A latitude/longitude pin |
|
|
192
|
+
| [`send_contacts!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/contacts.md) | A rich, vCard-like contact card |
|
|
193
|
+
| [`send_address!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/address.md) | A delivery-address form (India & Singapore only) |
|
|
194
|
+
| [`send_location_request!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/location_request.md) | A prompt asking the user to share their location |
|
|
195
|
+
| [`send_template!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/template.md) | A pre-approved marketing/utility/authentication template |
|
|
196
|
+
| [`send_interactive!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/interactive.md) | Reply buttons, lists, CTA URLs, or carousels |
|
|
197
|
+
| [`mark_message_as_read!`](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/mark_message_as_read.md) | Closes the read-receipt loop on an inbound message |
|
|
222
198
|
|
|
223
|
-
|
|
199
|
+
Each accepts an optional `client:` and returns a `Whatsapp::Messages::Response` with
|
|
200
|
+
typed `#contacts` and `#messages`. Invalid input raises
|
|
201
|
+
`ActiveModel::ValidationError` before any request is made.
|
|
224
202
|
|
|
225
|
-
|
|
226
|
-
Whatsapp::Messages.send_address!(
|
|
227
|
-
to: "+15551234567",
|
|
228
|
-
body: "Please share your delivery address",
|
|
229
|
-
country: "IN",
|
|
230
|
-
footer: "Thanks for shopping with us",
|
|
231
|
-
values: { name: "Jane Doe", city: "Bangalore" }
|
|
232
|
-
)
|
|
233
|
-
```
|
|
203
|
+
→ **[Sending messages](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/messages/README.md)**
|
|
234
204
|
|
|
235
|
-
|
|
205
|
+
## 📋 Managing Templates
|
|
236
206
|
|
|
237
|
-
|
|
207
|
+
Sending a template requires one that already exists and has been approved by Meta.
|
|
208
|
+
`Whatsapp::MessageTemplates` creates and manages those, so they live in your codebase
|
|
209
|
+
and ship from CI instead of being clicked together in WhatsApp Manager.
|
|
238
210
|
|
|
239
211
|
```ruby
|
|
240
|
-
Whatsapp::
|
|
241
|
-
```
|
|
212
|
+
templates = Whatsapp::MessageTemplates.new # needs waba_id
|
|
242
213
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
### Template
|
|
246
|
-
|
|
247
|
-
```ruby
|
|
248
|
-
Whatsapp::Messages.send_template!(
|
|
249
|
-
to: "+15551234567",
|
|
250
|
-
name: "order_confirmation",
|
|
251
|
-
language: { code: "en_US" },
|
|
214
|
+
created = templates.create(
|
|
215
|
+
name: "order_confirmation", language: "en_US", category: "UTILITY",
|
|
252
216
|
components: [
|
|
253
|
-
{ type:
|
|
217
|
+
{ type: :body,
|
|
218
|
+
text: "Thank you, {{1}}! Your order number is {{2}}.",
|
|
219
|
+
example: ["Pablo", "860198-230332"] },
|
|
220
|
+
{ type: :buttons, buttons: [
|
|
221
|
+
{ type: :url, text: "Track order", url: "https://example.com/orders/{{1}}", example: "1234" },
|
|
222
|
+
] },
|
|
254
223
|
]
|
|
255
224
|
)
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
`name` and `language` are required — `language.code` is validated against `Whatsapp::Utils::LanguageCodes`. `components` is optional and follows the Cloud API's `header`/`body`/`button` component shape, each with typed `parameters` (`text`, `currency`, `date_time`, `image`, `document`, `video`, `location`, or `payload`).
|
|
259
|
-
|
|
260
|
-
### Interactive
|
|
261
|
-
|
|
262
|
-
Interactive messages share one class, `Whatsapp::Messages::Interactive`; the `type:` you pass selects which action shape `action:` must match. All variants accept an optional `header:` (`text`, `image`, `video`, or `document`) and `footer:`.
|
|
263
225
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
```ruby
|
|
267
|
-
Whatsapp::Messages.send_interactive!(
|
|
268
|
-
to: "+15551234567",
|
|
269
|
-
type: :reply_buttons,
|
|
270
|
-
body: "Would you like to confirm your order?",
|
|
271
|
-
action: { buttons: [{ id: "confirm", title: "Confirm" }, { id: "cancel", title: "Cancel" }] }
|
|
272
|
-
)
|
|
226
|
+
created.status # => "PENDING" — Meta reviews asynchronously, up to 24 hours
|
|
273
227
|
```
|
|
274
228
|
|
|
275
|
-
|
|
229
|
+
Meta's rules are checked before the request, so a mistake raises immediately instead of
|
|
230
|
+
costing a review cycle:
|
|
276
231
|
|
|
277
232
|
```ruby
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
body: "Choose a drink",
|
|
282
|
-
action: {
|
|
283
|
-
button: "Menu",
|
|
284
|
-
sections: [
|
|
285
|
-
{ title: "Coffee", rows: [{ id: "espresso", title: "Espresso", description: "Strong & short" }] },
|
|
286
|
-
],
|
|
287
|
-
}
|
|
288
|
-
)
|
|
233
|
+
templates.create(name: "Order Confirmation", ...)
|
|
234
|
+
# => ActiveModel::ValidationError: Name must contain only lowercase alphanumeric
|
|
235
|
+
# characters and underscores
|
|
289
236
|
```
|
|
290
237
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
type: :url_button,
|
|
297
|
-
body: "Check out our new arrivals",
|
|
298
|
-
action: { name: "cta_url", display_text: "Shop now", url: "https://example.com/new" }
|
|
299
|
-
)
|
|
300
|
-
```
|
|
238
|
+
Covers standard, [authentication/OTP](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/message_templates/authentication.md),
|
|
239
|
+
[carousel](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/message_templates/carousel.md),
|
|
240
|
+
[limited-time offer](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/message_templates/limited_time_offer.md), and
|
|
241
|
+
[library](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/message_templates/library.md) templates,
|
|
242
|
+
plus list, find, update, and delete.
|
|
301
243
|
|
|
302
|
-
|
|
244
|
+
→ **[Managing templates](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/message_templates/README.md)**
|
|
303
245
|
|
|
304
|
-
|
|
305
|
-
Whatsapp::Messages.send_interactive!(
|
|
306
|
-
to: "+15551234567",
|
|
307
|
-
type: :media_carousel,
|
|
308
|
-
body: "Today's picks",
|
|
309
|
-
action: {
|
|
310
|
-
cards: [
|
|
311
|
-
{
|
|
312
|
-
header: { type: "image", link: "https://example.com/1.jpg" },
|
|
313
|
-
body: "Item one",
|
|
314
|
-
action: { name: "cta_url", display_text: "Buy", url: "https://example.com/1" },
|
|
315
|
-
buttons: [{ quick_reply: { id: "q1", title: "Details" } }],
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
header: { type: "image", link: "https://example.com/2.jpg" },
|
|
319
|
-
body: "Item two",
|
|
320
|
-
action: { name: "cta_url", display_text: "Buy", url: "https://example.com/2" },
|
|
321
|
-
buttons: [{ quick_reply: { id: "q2", title: "Details" } }],
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
}
|
|
325
|
-
)
|
|
326
|
-
```
|
|
246
|
+
## 🔔 Webhooks
|
|
327
247
|
|
|
328
|
-
|
|
248
|
+
Meta pushes inbound messages, delivery statuses, and ~18 other notification types to a
|
|
249
|
+
callback URL you register. Inside a Rails app:
|
|
329
250
|
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
to: "+15551234567",
|
|
333
|
-
type: :product_carousel,
|
|
334
|
-
body: "Recommended for you",
|
|
335
|
-
action: {
|
|
336
|
-
cards: [
|
|
337
|
-
{ catalog_id: "123456789", product_retailer_id: "SKU-1" },
|
|
338
|
-
{ catalog_id: "123456789", product_retailer_id: "SKU-2" },
|
|
339
|
-
],
|
|
340
|
-
}
|
|
341
|
-
)
|
|
251
|
+
```bash
|
|
252
|
+
bundle exec rake whatsapp:install:webhook
|
|
342
253
|
```
|
|
343
254
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
### Mark Message As Read
|
|
255
|
+
That copies a personalizable controller to
|
|
256
|
+
`app/controllers/whatsapp/webhooks_controller.rb` and prints the routes to add:
|
|
347
257
|
|
|
348
258
|
```ruby
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
259
|
+
def receive
|
|
260
|
+
raw_body = request.body.read
|
|
261
|
+
return head(:unauthorized) unless Whatsapp::Webhook::Signature.valid?(
|
|
262
|
+
payload: raw_body, header: request.headers["X-Hub-Signature-256"]
|
|
263
|
+
)
|
|
353
264
|
|
|
354
|
-
|
|
265
|
+
notification = Whatsapp::Webhook::Notification.deserialize(JSON.parse(raw_body))
|
|
355
266
|
|
|
356
|
-
|
|
267
|
+
notification.entry.each do |entry|
|
|
268
|
+
entry.changes.each { |change| WebhookJob.perform_later(change) }
|
|
269
|
+
end
|
|
357
270
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
"messaging_product": "whatsapp",
|
|
361
|
-
"contacts": [{ "input": "+15551234567", "wa_id": "15551234567" }],
|
|
362
|
-
"messages": [{ "id": "wamid.HBgLMTU1NTU1NTU1NTUV..." }]
|
|
363
|
-
}
|
|
271
|
+
head :ok
|
|
272
|
+
end
|
|
364
273
|
```
|
|
365
274
|
|
|
275
|
+
Every notification deserializes into typed objects — no raw hash spelunking:
|
|
276
|
+
|
|
366
277
|
```ruby
|
|
367
|
-
|
|
278
|
+
message = change.value.messages.first
|
|
368
279
|
|
|
369
|
-
|
|
370
|
-
|
|
280
|
+
message.from # => "16505551234"
|
|
281
|
+
message.id # => "wamid.HBg..."
|
|
282
|
+
message.body # => "Does it come in another color?"
|
|
371
283
|
```
|
|
372
284
|
|
|
373
|
-
|
|
285
|
+
All 19 documented fields get a class. Only `messages` has a Meta-published schema; the
|
|
286
|
+
other 18 are best-effort and flagged as such, per field.
|
|
287
|
+
|
|
288
|
+
→ **[Webhooks](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/webhooks/README.md)**
|
|
289
|
+
· [inbound messages & statuses](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/webhooks/messages.md)
|
|
374
290
|
|
|
375
|
-
## Media
|
|
291
|
+
## 📷 Media
|
|
376
292
|
|
|
377
293
|
```ruby
|
|
378
294
|
media = Whatsapp::Media.new
|
|
379
295
|
|
|
380
296
|
media_id = media.upload(file_path: "photo.jpg", type: "image/jpeg")
|
|
381
|
-
info = media.get_url(media_id: media_id)
|
|
382
|
-
media.download(url: info["url"], save_to: "photo.jpg")
|
|
383
|
-
media.delete(media_id: media_id)
|
|
297
|
+
info = media.get_url(media_id: media_id)
|
|
298
|
+
media.download(url: info["url"], save_to: "photo.jpg")
|
|
299
|
+
media.delete(media_id: media_id) # => true
|
|
384
300
|
```
|
|
385
301
|
|
|
386
302
|
`download` refuses to attach the API token to a non-HTTPS URL or a host that is not on
|
|
387
|
-
`Configuration#media_host_allowlist`, so a token is never sent to an
|
|
303
|
+
`Configuration#media_host_allowlist`, so a token is never sent to an
|
|
304
|
+
attacker-influenced URL. Bodies stream to disk rather than buffering in memory.
|
|
388
305
|
|
|
389
|
-
|
|
306
|
+
→ **[Media](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/media/README.md)**
|
|
390
307
|
|
|
391
|
-
|
|
392
|
-
Meta. `Whatsapp::MessageTemplates` creates and manages those templates, so they can live in
|
|
393
|
-
your codebase and ship from CI instead of being clicked together in WhatsApp Manager.
|
|
308
|
+
## 🔌 Subscribed Apps
|
|
394
309
|
|
|
395
|
-
|
|
396
|
-
|
|
310
|
+
Before your app receives any webhook notifications for a business account, it has to be
|
|
311
|
+
subscribed to it:
|
|
397
312
|
|
|
398
313
|
```ruby
|
|
399
|
-
Whatsapp.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
templates = Whatsapp::MessageTemplates.new
|
|
314
|
+
Whatsapp::SubscribedApp::Subscribe.call # start webhook delivery
|
|
315
|
+
Whatsapp::SubscribedApp::List.call.map(&:name) # => ["My App"]
|
|
316
|
+
Whatsapp::SubscribedApp::Unsubscribe.call # stop it
|
|
405
317
|
```
|
|
406
318
|
|
|
407
|
-
|
|
319
|
+
Tech Providers routing several accounts to different callback URLs pass an
|
|
320
|
+
`override_callback_uri:`.
|
|
408
321
|
|
|
409
|
-
|
|
410
|
-
created = templates.create(
|
|
411
|
-
name: "order_confirmation", # lowercase alphanumerics and underscores only
|
|
412
|
-
language: "en_US",
|
|
413
|
-
category: "UTILITY", # UTILITY | MARKETING | AUTHENTICATION
|
|
414
|
-
components: [
|
|
415
|
-
{ type: :header, format: "TEXT", text: "Order {{1}} confirmed", example: ["#1234"] },
|
|
416
|
-
{ type: :body,
|
|
417
|
-
text: "Thank you, {{1}}! Your order number is {{2}}.",
|
|
418
|
-
example: ["Pablo", "860198-230332"] },
|
|
419
|
-
{ type: :footer, text: "Thanks for shopping with us" },
|
|
420
|
-
{ type: :buttons, buttons: [
|
|
421
|
-
{ type: :phone_number, text: "Call", phone_number: "15550051310" },
|
|
422
|
-
{ type: :url, text: "Track order", url: "https://example.com/orders/{{1}}", example: "1234" },
|
|
423
|
-
] },
|
|
424
|
-
]
|
|
425
|
-
)
|
|
322
|
+
→ **[Subscribed apps](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/subscribed_app/README.md)**
|
|
426
323
|
|
|
427
|
-
|
|
428
|
-
created.status # => "PENDING" — Meta reviews asynchronously, up to 24 hours
|
|
429
|
-
created.pending? # => true
|
|
430
|
-
```
|
|
324
|
+
## 📞 Business Phone Numbers
|
|
431
325
|
|
|
432
|
-
|
|
326
|
+
A phone number is unusable with Cloud API until it is **registered** — the prerequisite
|
|
327
|
+
that makes sending, media, and templates work for it at all.
|
|
433
328
|
|
|
434
|
-
```ruby
|
|
435
|
-
templates.create(
|
|
436
|
-
name: "order_confirmation", language: "en_US", category: "UTILITY",
|
|
437
|
-
parameter_format: "NAMED",
|
|
438
|
-
components: [
|
|
439
|
-
{ type: :body,
|
|
440
|
-
text: "Thank you, {{first_name}}! Your order number is {{order_number}}.",
|
|
441
|
-
example: { first_name: "Pablo", order_number: "860198-230332" } },
|
|
442
|
-
]
|
|
443
|
-
)
|
|
444
329
|
```
|
|
330
|
+
RequestCode -> VerifyCode -> Register (onboarding)
|
|
331
|
+
(send OTP) (confirm it) (activate on Cloud API)
|
|
445
332
|
|
|
446
|
-
|
|
447
|
-
costing a review cycle:
|
|
448
|
-
|
|
449
|
-
```ruby
|
|
450
|
-
templates.create(name: "Order Confirmation", ...)
|
|
451
|
-
# => ActiveModel::ValidationError: Name must contain only lowercase alphanumeric
|
|
452
|
-
# characters and underscores
|
|
453
|
-
|
|
454
|
-
templates.create(..., components: [{ type: :body, text: "Hi {{1}} and {{2}}", example: ["Pablo"] }])
|
|
455
|
-
# => ActiveModel::ValidationError: Example does not match the body text:
|
|
456
|
-
# 2 placeholders but 1 example
|
|
333
|
+
Deregister (the reverse switch)
|
|
457
334
|
```
|
|
458
335
|
|
|
459
|
-
### Listing, reading, editing, deleting
|
|
460
|
-
|
|
461
336
|
```ruby
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
template = templates.find(template_id: "1259544702043867")
|
|
468
|
-
template.status # => "APPROVED"
|
|
469
|
-
template.editable? # => true (APPROVED, REJECTED and PAUSED templates can be edited)
|
|
470
|
-
|
|
471
|
-
templates.update(template_id: template.id, category: "MARKETING") # => true
|
|
472
|
-
templates.update(template_id: template.id, components: [...]) # full replacement
|
|
473
|
-
|
|
474
|
-
templates.delete(name: "order_confirmation") # every language variant
|
|
475
|
-
templates.delete(hsm_id: "1407680676729941", name: "order_confirmation")
|
|
476
|
-
templates.delete(hsm_ids: %w[1387372356726668 1304694804498707]) # up to 100
|
|
337
|
+
Whatsapp::BusinessPhoneNumber::RequestCode.call(code_method: "SMS", language: "en_US")
|
|
338
|
+
Whatsapp::BusinessPhoneNumber::VerifyCode.call(code: "123456")
|
|
339
|
+
Whatsapp::BusinessPhoneNumber::Register.call(pin: "212834")
|
|
340
|
+
Whatsapp::BusinessPhoneNumber::Deregister.call
|
|
477
341
|
```
|
|
478
342
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
343
|
+
The 6-digit two-step verification PIN and the local-storage region are validated
|
|
344
|
+
client-side — which matters here, because a rejected attempt still counts against a
|
|
345
|
+
rate limit of 10 requests per number per 72-hour window.
|
|
482
346
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
**Authentication (OTP)** templates invert the usual shape — Meta supplies and localises the
|
|
486
|
-
wording, so you pass flags rather than text, and `upsert` creates every language at once:
|
|
347
|
+
The account that number belongs to is readable and writable too — the one part of this
|
|
348
|
+
module that addresses `waba_id` rather than `phone_id`:
|
|
487
349
|
|
|
488
350
|
```ruby
|
|
489
|
-
|
|
490
|
-
name: "authentication_code", languages: %w[en_US es_ES fr], category: "AUTHENTICATION",
|
|
491
|
-
components: [
|
|
492
|
-
{ type: :body, add_security_recommendation: true },
|
|
493
|
-
{ type: :footer, code_expiration_minutes: 15 },
|
|
494
|
-
{ type: :buttons, buttons: [{ type: :otp, otp_type: "COPY_CODE" }] },
|
|
495
|
-
]
|
|
496
|
-
)
|
|
497
|
-
```
|
|
351
|
+
details = Whatsapp::BusinessPhoneNumber::Account::Get.call(fields: %w[name account_review_status])
|
|
498
352
|
|
|
499
|
-
|
|
353
|
+
details.name # => "Acme Corp"
|
|
354
|
+
details.approved? # => true
|
|
500
355
|
|
|
501
|
-
|
|
502
|
-
card = {
|
|
503
|
-
header: { format: "IMAGE", header_handle: "4::aW..." },
|
|
504
|
-
body: { text: "Rare {{1}} in stock!", example: ["Tulips"] },
|
|
505
|
-
buttons: [{ type: :quick_reply, text: "More like this" }],
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
templates.create(
|
|
509
|
-
name: "summer_carousel", language: "en_US", category: "MARKETING",
|
|
510
|
-
components: [
|
|
511
|
-
{ type: :body, text: "Summer is here, {{1}}!", example: ["Pablo"] },
|
|
512
|
-
{ type: :carousel, cards: [card, card] },
|
|
513
|
-
]
|
|
514
|
-
)
|
|
356
|
+
Whatsapp::BusinessPhoneNumber::Account::Update.call(name: "Acme Corporation").success # => true
|
|
515
357
|
```
|
|
516
358
|
|
|
517
|
-
**
|
|
518
|
-
|
|
359
|
+
And so is the **business profile** — the card a user sees before they reply. It addresses
|
|
360
|
+
the same `phone_id` as the onboarding actions:
|
|
519
361
|
|
|
520
362
|
```ruby
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
components: [
|
|
524
|
-
{ type: :header, format: "IMAGE", header_handle: "4::aW..." },
|
|
525
|
-
{ type: :limited_time_offer, text: "Expiring offer!", has_expiration: true },
|
|
526
|
-
{ type: :body, text: "Good news, {{1}}! Use code {{2}} for 25% off.",
|
|
527
|
-
example: ["Pablo", "SPRING25"] },
|
|
528
|
-
{ type: :buttons, buttons: [
|
|
529
|
-
{ type: :copy_code, example: "SPRING25" },
|
|
530
|
-
{ type: :url, text: "Book now!", url: "https://example.com/o?c={{1}}", example: "n3mtql" },
|
|
531
|
-
] },
|
|
532
|
-
]
|
|
533
|
-
)
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
**Library templates** are pre-written and pre-approved by Meta, so they usually come back
|
|
537
|
-
`APPROVED` immediately:
|
|
363
|
+
Whatsapp::BusinessPhoneNumber::Profile::Get.call.about
|
|
364
|
+
# => "Open daily 9-5"
|
|
538
365
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
library_template_button_inputs: [
|
|
544
|
-
{ type: "URL", url: { base_url: "https://example.com/{{1}}",
|
|
545
|
-
url_suffix_example: "https://example.com/order_update" } },
|
|
546
|
-
]
|
|
547
|
-
)
|
|
366
|
+
Whatsapp::BusinessPhoneNumber::Profile::Update.call(
|
|
367
|
+
about: "Open daily 9-6", vertical: "RETAIL", websites: ["https://acme.test"]
|
|
368
|
+
).success
|
|
369
|
+
# => true
|
|
548
370
|
```
|
|
549
371
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
- **Media headers** take a `header_handle` you already hold. Producing one needs Meta's
|
|
553
|
-
Resumable Upload API, which this gem does not wrap — note it is a different flow from
|
|
554
|
-
[`Media#upload`](#media), whose media IDs are for *sending*, not template creation.
|
|
555
|
-
- **Review outcomes arrive by webhook**, not by polling: see
|
|
556
|
-
`message_template_status_update` and friends under [Webhooks](#webhooks).
|
|
557
|
-
- Text containing `#{{1}}` needs single quotes in Ruby, or `#{` starts interpolation.
|
|
372
|
+
Character limits, the 21-value vertical enum, and the two-website cap are all checked
|
|
373
|
+
before the request goes out.
|
|
558
374
|
|
|
559
|
-
|
|
375
|
+
→ **[Business phone numbers](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/README.md)**
|
|
376
|
+
· [the business account](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/account.md)
|
|
377
|
+
· [the business profile](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/business_phone_number/profile.md)
|
|
560
378
|
|
|
561
|
-
|
|
562
|
-
notification types to a callback URL you register. Inside a Rails app:
|
|
379
|
+
## 🚨 Errors
|
|
563
380
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
```
|
|
381
|
+
Everything descends from `Whatsapp::Error`, and each module raises its own subclass so
|
|
382
|
+
you can rescue narrowly:
|
|
567
383
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
# config/initializers/whatsapp.rb
|
|
577
|
-
Whatsapp.configure do |config|
|
|
578
|
-
config.verify_token = Rails.application.credentials.whatsapp_verify_token
|
|
579
|
-
config.app_secret = Rails.application.credentials.whatsapp_app_secret
|
|
580
|
-
end
|
|
581
|
-
```
|
|
384
|
+
| Class | Raised by |
|
|
385
|
+
| --- | --- |
|
|
386
|
+
| `Whatsapp::RequestError` | A failed message send |
|
|
387
|
+
| `Whatsapp::Messages::PayloadError` | An unknown message kind |
|
|
388
|
+
| `Whatsapp::Media::MediaError` | Anything in `Media` |
|
|
389
|
+
| `Whatsapp::MessageTemplates::TemplateError` | Anything in `MessageTemplates` |
|
|
390
|
+
| `Whatsapp::SubscribedApp::Error` | Anything in `SubscribedApp` |
|
|
391
|
+
| `Whatsapp::BusinessPhoneNumber::Error` | Anything in `BusinessPhoneNumber`, including `Account` |
|
|
582
392
|
|
|
583
|
-
|
|
584
|
-
|
|
393
|
+
Local validation failures raise `ActiveModel::ValidationError` instead — they happen at
|
|
394
|
+
construction time, before any network call, and carry per-attribute detail:
|
|
585
395
|
|
|
586
396
|
```ruby
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
challenge ? render(plain: challenge) : head(:forbidden)
|
|
591
|
-
end
|
|
592
|
-
|
|
593
|
-
def receive
|
|
594
|
-
raw_body = request.body.read
|
|
595
|
-
return head(:unauthorized) unless Whatsapp::Webhook::Signature.valid?(
|
|
596
|
-
payload: raw_body, header: request.headers["X-Hub-Signature-256"]
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
notification = Whatsapp::Webhook::Notification.deserialize(JSON.parse(raw_body))
|
|
600
|
-
# notification.entry.each { |entry| entry.changes.each { |change| WebhookJob.perform_later(change) } }
|
|
601
|
-
|
|
602
|
-
head :ok
|
|
603
|
-
end
|
|
604
|
-
end
|
|
397
|
+
rescue ActiveModel::ValidationError => e
|
|
398
|
+
e.model.errors.full_messages
|
|
399
|
+
# => ["Caption is too long (maximum is 1024 characters)"]
|
|
605
400
|
```
|
|
606
401
|
|
|
607
|
-
|
|
608
|
-
`value` — for the `messages` field, `value.messages` and `value.statuses` are arrays of typed
|
|
609
|
-
message/status objects (`Whatsapp::Webhook::Message::Text`, `Whatsapp::Webhook::Status`, etc.).
|
|
610
|
-
The other ~18 documented fields (`account_alerts`, `message_template_status_update`, and so on)
|
|
611
|
-
each deserialize into their own best-effort typed class — see
|
|
612
|
-
[`lib/ruby/whatsapp/webhook/CLAUDE.md`](lib/ruby/whatsapp/webhook/CLAUDE.md) for the full field
|
|
613
|
-
reference and confidence notes, since Meta's docs don't publish a JSON schema for most of them.
|
|
402
|
+
→ **[Errors](https://github.com/saleszera/ruby-whatsapp/blob/main/docs/errors.md)**
|
|
614
403
|
|
|
615
|
-
|
|
616
|
-
`app_secret:` explicitly instead of relying on the global config default:
|
|
404
|
+
## 🧩 Compatibility
|
|
617
405
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
See Meta's [webhook documentation](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview) for the full notification catalog, retry behavior, and signature details.
|
|
406
|
+
| | |
|
|
407
|
+
| --- | --- |
|
|
408
|
+
| Ruby | >= 3.2 (CI runs 3.2 and 3.4) |
|
|
409
|
+
| Graph API | `v24.0` by default, overridable |
|
|
410
|
+
| Rails | Optional. Webhook controller generator activates when Rails is loaded |
|
|
411
|
+
| Dependencies | `activemodel`, `http`, `logger`, `zeitwerk` |
|
|
625
412
|
|
|
626
|
-
## Development
|
|
413
|
+
## 🔨 Development
|
|
627
414
|
|
|
628
415
|
After checking out the repo, run `bundle install`, then:
|
|
629
416
|
|
|
630
417
|
```bash
|
|
631
|
-
bundle exec rake #
|
|
418
|
+
bundle exec rake # specs + RuboCop (the default task)
|
|
632
419
|
bundle exec rspec # specs only
|
|
633
420
|
bundle exec rubocop # lint only
|
|
634
421
|
bin/console # interactive prompt
|
|
635
422
|
```
|
|
636
423
|
|
|
637
|
-
## Contributing
|
|
424
|
+
## 🤝 Contributing
|
|
425
|
+
|
|
426
|
+
Bug reports and pull requests are welcome at
|
|
427
|
+
<https://github.com/saleszera/ruby-whatsapp>. Please write the failing spec first —
|
|
428
|
+
this gem is developed test-first — and make sure `bundle exec rake` is green before
|
|
429
|
+
opening a PR.
|
|
430
|
+
|
|
431
|
+
## 📄 License
|
|
638
432
|
|
|
639
|
-
|
|
433
|
+
Available as open source under the terms of the
|
|
434
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
|
640
435
|
|
|
641
|
-
|
|
436
|
+
---
|
|
642
437
|
|
|
643
|
-
|
|
438
|
+

|