ruby-whatsapp 0.3.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +54 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +15 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +643 -0
- data/Rakefile +12 -0
- data/lib/ruby/whatsapp/client.rb +82 -0
- data/lib/ruby/whatsapp/configuration.rb +79 -0
- data/lib/ruby/whatsapp/instrumentation.rb +34 -0
- data/lib/ruby/whatsapp/media.rb +134 -0
- data/lib/ruby/whatsapp/message_templates/button/base.rb +26 -0
- data/lib/ruby/whatsapp/message_templates/button/copy_code.rb +59 -0
- data/lib/ruby/whatsapp/message_templates/button/otp/supported_app.rb +44 -0
- data/lib/ruby/whatsapp/message_templates/button/otp.rb +138 -0
- data/lib/ruby/whatsapp/message_templates/button/phone_number.rb +45 -0
- data/lib/ruby/whatsapp/message_templates/button/quick_reply.rb +38 -0
- data/lib/ruby/whatsapp/message_templates/button/url.rb +110 -0
- data/lib/ruby/whatsapp/message_templates/button.rb +56 -0
- data/lib/ruby/whatsapp/message_templates/categories.rb +47 -0
- data/lib/ruby/whatsapp/message_templates/component/base.rb +112 -0
- data/lib/ruby/whatsapp/message_templates/component/body.rb +139 -0
- data/lib/ruby/whatsapp/message_templates/component/buttons.rb +95 -0
- data/lib/ruby/whatsapp/message_templates/component/carousel/card.rb +100 -0
- data/lib/ruby/whatsapp/message_templates/component/carousel.rb +71 -0
- data/lib/ruby/whatsapp/message_templates/component/footer.rb +99 -0
- data/lib/ruby/whatsapp/message_templates/component/header.rb +188 -0
- data/lib/ruby/whatsapp/message_templates/component/limited_time_offer.rb +57 -0
- data/lib/ruby/whatsapp/message_templates/component.rb +62 -0
- data/lib/ruby/whatsapp/message_templates/component_set.rb +221 -0
- data/lib/ruby/whatsapp/message_templates/example.rb +148 -0
- data/lib/ruby/whatsapp/message_templates/library_template/body_inputs.rb +81 -0
- data/lib/ruby/whatsapp/message_templates/library_template/button_inputs.rb +146 -0
- data/lib/ruby/whatsapp/message_templates/library_template.rb +114 -0
- data/lib/ruby/whatsapp/message_templates/parameter_formats.rb +49 -0
- data/lib/ruby/whatsapp/message_templates/placeholders.rb +86 -0
- data/lib/ruby/whatsapp/message_templates/response/collection.rb +74 -0
- data/lib/ruby/whatsapp/message_templates/response/created.rb +65 -0
- data/lib/ruby/whatsapp/message_templates/response/node.rb +152 -0
- data/lib/ruby/whatsapp/message_templates/response/paging.rb +39 -0
- data/lib/ruby/whatsapp/message_templates/response/quality_score.rb +49 -0
- data/lib/ruby/whatsapp/message_templates/response/summary.rb +68 -0
- data/lib/ruby/whatsapp/message_templates/response.rb +22 -0
- data/lib/ruby/whatsapp/message_templates/statuses.rb +66 -0
- data/lib/ruby/whatsapp/message_templates/template.rb +180 -0
- data/lib/ruby/whatsapp/message_templates/value_object.rb +62 -0
- data/lib/ruby/whatsapp/message_templates.rb +283 -0
- data/lib/ruby/whatsapp/messages/address.rb +103 -0
- data/lib/ruby/whatsapp/messages/audio.rb +64 -0
- data/lib/ruby/whatsapp/messages/base.rb +46 -0
- data/lib/ruby/whatsapp/messages/contacts/address.rb +84 -0
- data/lib/ruby/whatsapp/messages/contacts/contact.rb +78 -0
- data/lib/ruby/whatsapp/messages/contacts/email.rb +49 -0
- data/lib/ruby/whatsapp/messages/contacts/name.rb +68 -0
- data/lib/ruby/whatsapp/messages/contacts/org.rb +41 -0
- data/lib/ruby/whatsapp/messages/contacts/phone.rb +59 -0
- data/lib/ruby/whatsapp/messages/contacts/url.rb +51 -0
- data/lib/ruby/whatsapp/messages/contacts.rb +37 -0
- data/lib/ruby/whatsapp/messages/document.rb +79 -0
- data/lib/ruby/whatsapp/messages/image.rb +72 -0
- data/lib/ruby/whatsapp/messages/interactive/base.rb +15 -0
- data/lib/ruby/whatsapp/messages/interactive/body.rb +39 -0
- data/lib/ruby/whatsapp/messages/interactive/footer.rb +40 -0
- data/lib/ruby/whatsapp/messages/interactive/header.rb +102 -0
- data/lib/ruby/whatsapp/messages/interactive/list_buttons/section/row.rb +66 -0
- data/lib/ruby/whatsapp/messages/interactive/list_buttons/section.rb +64 -0
- data/lib/ruby/whatsapp/messages/interactive/list_buttons.rb +63 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel/card/button/quick_reply.rb +50 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel/card/button.rb +47 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel/card.rb +78 -0
- data/lib/ruby/whatsapp/messages/interactive/media_carousel.rb +50 -0
- data/lib/ruby/whatsapp/messages/interactive/product_carousel/card.rb +55 -0
- data/lib/ruby/whatsapp/messages/interactive/product_carousel.rb +50 -0
- data/lib/ruby/whatsapp/messages/interactive/reply_buttons/button.rb +59 -0
- data/lib/ruby/whatsapp/messages/interactive/reply_buttons.rb +47 -0
- data/lib/ruby/whatsapp/messages/interactive/url_button.rb +68 -0
- data/lib/ruby/whatsapp/messages/interactive.rb +112 -0
- data/lib/ruby/whatsapp/messages/location.rb +67 -0
- data/lib/ruby/whatsapp/messages/location_request.rb +41 -0
- data/lib/ruby/whatsapp/messages/mark_message_as_read.rb +42 -0
- data/lib/ruby/whatsapp/messages/reaction.rb +61 -0
- data/lib/ruby/whatsapp/messages/response/contacts.rb +36 -0
- data/lib/ruby/whatsapp/messages/response/messages.rb +27 -0
- data/lib/ruby/whatsapp/messages/response.rb +49 -0
- data/lib/ruby/whatsapp/messages/sticker.rb +62 -0
- data/lib/ruby/whatsapp/messages/template/component.rb +67 -0
- data/lib/ruby/whatsapp/messages/template/language.rb +55 -0
- data/lib/ruby/whatsapp/messages/template/parameter.rb +113 -0
- data/lib/ruby/whatsapp/messages/template.rb +62 -0
- data/lib/ruby/whatsapp/messages/text.rb +43 -0
- data/lib/ruby/whatsapp/messages/video.rb +68 -0
- data/lib/ruby/whatsapp/messages.rb +116 -0
- data/lib/ruby/whatsapp/railtie.rb +14 -0
- data/lib/ruby/whatsapp/request_error.rb +5 -0
- data/lib/ruby/whatsapp/response_handling.rb +44 -0
- data/lib/ruby/whatsapp/utils/language_codes.rb +96 -0
- data/lib/ruby/whatsapp/version.rb +5 -0
- data/lib/ruby/whatsapp/webhook/account_alerts.rb +58 -0
- data/lib/ruby/whatsapp/webhook/account_review_update.rb +27 -0
- data/lib/ruby/whatsapp/webhook/account_update/ban_info.rb +33 -0
- data/lib/ruby/whatsapp/webhook/account_update.rb +46 -0
- data/lib/ruby/whatsapp/webhook/automatic_events.rb +43 -0
- data/lib/ruby/whatsapp/webhook/business_capability_update.rb +36 -0
- data/lib/ruby/whatsapp/webhook/change.rb +61 -0
- data/lib/ruby/whatsapp/webhook/contact.rb +36 -0
- data/lib/ruby/whatsapp/webhook/entry.rb +35 -0
- data/lib/ruby/whatsapp/webhook/error.rb +47 -0
- data/lib/ruby/whatsapp/webhook/history.rb +54 -0
- data/lib/ruby/whatsapp/webhook/installer.rb +61 -0
- data/lib/ruby/whatsapp/webhook/message/audio.rb +28 -0
- data/lib/ruby/whatsapp/webhook/message/base.rb +61 -0
- data/lib/ruby/whatsapp/webhook/message/button.rb +33 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/address.rb +68 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +68 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/email.rb +35 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/name.rb +62 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/org.rb +40 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/phone.rb +40 -0
- data/lib/ruby/whatsapp/webhook/message/contacts/url.rb +35 -0
- data/lib/ruby/whatsapp/webhook/message/contacts.rb +31 -0
- data/lib/ruby/whatsapp/webhook/message/context.rb +49 -0
- data/lib/ruby/whatsapp/webhook/message/document.rb +32 -0
- data/lib/ruby/whatsapp/webhook/message/image.rb +18 -0
- data/lib/ruby/whatsapp/webhook/message/interactive.rb +53 -0
- data/lib/ruby/whatsapp/webhook/message/location.rb +49 -0
- data/lib/ruby/whatsapp/webhook/message/media.rb +54 -0
- data/lib/ruby/whatsapp/webhook/message/order/product_item.rb +50 -0
- data/lib/ruby/whatsapp/webhook/message/order.rb +43 -0
- data/lib/ruby/whatsapp/webhook/message/reaction.rb +37 -0
- data/lib/ruby/whatsapp/webhook/message/referral.rb +80 -0
- data/lib/ruby/whatsapp/webhook/message/sticker.rb +32 -0
- data/lib/ruby/whatsapp/webhook/message/system.rb +49 -0
- data/lib/ruby/whatsapp/webhook/message/text.rb +28 -0
- data/lib/ruby/whatsapp/webhook/message/unknown.rb +39 -0
- data/lib/ruby/whatsapp/webhook/message/video.rb +18 -0
- data/lib/ruby/whatsapp/webhook/message.rb +39 -0
- data/lib/ruby/whatsapp/webhook/message_template_components_update.rb +53 -0
- data/lib/ruby/whatsapp/webhook/message_template_quality_update.rb +59 -0
- data/lib/ruby/whatsapp/webhook/message_template_status_update.rb +54 -0
- data/lib/ruby/whatsapp/webhook/messages.rb +55 -0
- data/lib/ruby/whatsapp/webhook/metadata.rb +35 -0
- data/lib/ruby/whatsapp/webhook/notification.rb +36 -0
- data/lib/ruby/whatsapp/webhook/partner_solutions.rb +36 -0
- data/lib/ruby/whatsapp/webhook/payment_configuration_update.rb +51 -0
- data/lib/ruby/whatsapp/webhook/phone_number_name_update.rb +51 -0
- data/lib/ruby/whatsapp/webhook/phone_number_quality_update.rb +45 -0
- data/lib/ruby/whatsapp/webhook/security.rb +41 -0
- data/lib/ruby/whatsapp/webhook/signature.rb +28 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +38 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -0
- data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +48 -0
- data/lib/ruby/whatsapp/webhook/status/conversation.rb +42 -0
- data/lib/ruby/whatsapp/webhook/status/pricing.rb +38 -0
- data/lib/ruby/whatsapp/webhook/status.rb +65 -0
- data/lib/ruby/whatsapp/webhook/template_category_update.rb +64 -0
- data/lib/ruby/whatsapp/webhook/templates/webhooks_controller.rb.tt +31 -0
- data/lib/ruby/whatsapp/webhook/unknown_field.rb +37 -0
- data/lib/ruby/whatsapp/webhook/user_preferences.rb +47 -0
- data/lib/ruby/whatsapp/webhook/verification.rb +27 -0
- data/lib/ruby/whatsapp/webhook.rb +8 -0
- data/lib/ruby/whatsapp.rb +49 -0
- data/lib/tasks/whatsapp.rake +10 -0
- data/sig/ruby/whatsapp.rbs +6 -0
- metadata +267 -0
data/README.md
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# ruby-whatsapp
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/ruby-whatsapp)
|
|
4
|
+
[](https://github.com/saleszera/ruby-whatsapp/actions/workflows/main.yml)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A small, dependency-light Ruby client for the [Meta WhatsApp Cloud API](https://developers.facebook.com/documentation/business-messaging/whatsapp). Every message type the Cloud API supports — text, media, location, contacts, templates, and the full family of interactive messages (reply buttons, lists, CTA URLs, carousels) — is modeled as its own `ActiveModel`-validated Ruby class, so a malformed payload raises locally instead of round-tripping to Meta's servers first. The gem also handles media upload/download and parses API responses into typed objects.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Configuration](#configuration)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [Features](#features)
|
|
15
|
+
- [Sending Messages](#sending-messages)
|
|
16
|
+
- [Text](#text)
|
|
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:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem "ruby-whatsapp"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then run `bundle install`. Or install directly:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gem install ruby-whatsapp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
Whatsapp.configure do |config|
|
|
56
|
+
config.api_key = ENV.fetch("WHATSAPP_TOKEN") # a Meta system-user / app access token
|
|
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
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`api_key`, `app_secret`, and `verify_token` are redacted from `Configuration#inspect`, so they will not leak into logs.
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
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:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hello!")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each `send_<kind>!` method accepts an optional `client:` keyword (defaults to a new `Whatsapp::Client` built from `Whatsapp.configuration`) and returns a `Whatsapp::Messages::Response` (see [Handling Responses](#handling-responses)). It raises `ActiveModel::ValidationError` if any field fails validation, and `Whatsapp::RequestError` on a non-2xx API response.
|
|
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!
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This form raises `Whatsapp::Messages::PayloadError` for an unknown `kind` (in addition to the same validation/request errors above).
|
|
115
|
+
|
|
116
|
+
| Method | Sends |
|
|
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)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`body` is required (max 4096 characters). `preview_url` defaults to `true`.
|
|
139
|
+
|
|
140
|
+
### Image
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
Whatsapp::Messages.send_image!(
|
|
144
|
+
to: "+15551234567",
|
|
145
|
+
link: "https://example.com/photo.jpg",
|
|
146
|
+
caption: "Our new product"
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Either `id` (an uploaded [media](#media) ID) or `link` is required. `caption` is optional (max 1024 characters).
|
|
151
|
+
|
|
152
|
+
### Video
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Whatsapp::Messages.send_video!(to: "+15551234567", id: "1234567890", caption: "Demo video")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Either `id` or `link` is required. `caption` is optional (max 1024 characters).
|
|
159
|
+
|
|
160
|
+
### Audio
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
Whatsapp::Messages.send_audio!(to: "+15551234567", link: "https://example.com/clip.mp3")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Either `id` or `link` is required. Audio messages do not support captions.
|
|
167
|
+
|
|
168
|
+
### Document
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
Whatsapp::Messages.send_document!(
|
|
172
|
+
to: "+15551234567",
|
|
173
|
+
link: "https://example.com/invoice.pdf",
|
|
174
|
+
filename: "invoice.pdf"
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Either `id` or `link` is required. `caption` (max 1024 characters) and `filename` are optional.
|
|
179
|
+
|
|
180
|
+
### Sticker
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
Whatsapp::Messages.send_sticker!(to: "+15551234567", id: "STICKER_MEDIA_ID")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Either `id` or `link` is required — prefer `id` (from [`Media#upload`](#media)) over `link` for performance.
|
|
187
|
+
|
|
188
|
+
### Reaction
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
Whatsapp::Messages.send_reaction!(to: "+15551234567", message_id: "wamid.HBg...", emoji: "👍")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`message_id` and `emoji` are required. Pass `emoji: ""` to remove a reaction you sent previously.
|
|
195
|
+
|
|
196
|
+
### Location
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
Whatsapp::Messages.send_location!(to: "+15551234567", latitude: 37.4847, longitude: -122.1477, name: "Meta HQ")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`latitude` and `longitude` are required. `name` and `address` are optional.
|
|
203
|
+
|
|
204
|
+
### Contacts
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
Whatsapp::Messages.send_contacts!(
|
|
208
|
+
to: "+15551234567",
|
|
209
|
+
contacts: [
|
|
210
|
+
{
|
|
211
|
+
name: { formatted_name: "Jane Doe", first_name: "Jane", last_name: "Doe" },
|
|
212
|
+
phones: [{ phone: "+15550001111", type: "WORK" }],
|
|
213
|
+
emails: [{ email: "jane@example.com", type: "WORK" }],
|
|
214
|
+
org: { company: "Acme Inc." },
|
|
215
|
+
birthday: "1990-05-12",
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`contacts` must contain exactly one contact — the Cloud API currently allows only one per message. Each contact requires a `name.formatted_name`; `phones`, `emails`, `addresses`, `org`, `urls`, and `birthday` are all optional.
|
|
222
|
+
|
|
223
|
+
### Address
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
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
|
+
```
|
|
234
|
+
|
|
235
|
+
Only available for businesses based in India (`country: "IN"`) or Singapore (`country: "SG"`). `body` and `country` are required; `footer`, `values`, and `saved_addresses` are optional.
|
|
236
|
+
|
|
237
|
+
### Location Request
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
Whatsapp::Messages.send_location_request!(to: "+15551234567", body: "Can you share your delivery location?")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Prompts the user to share their current location. `body` is required.
|
|
244
|
+
|
|
245
|
+
### Template
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
Whatsapp::Messages.send_template!(
|
|
249
|
+
to: "+15551234567",
|
|
250
|
+
name: "order_confirmation",
|
|
251
|
+
language: { code: "en_US" },
|
|
252
|
+
components: [
|
|
253
|
+
{ type: "body", parameters: [{ type: "text", text: "Jane" }, { type: "text", text: "#1234" }] },
|
|
254
|
+
]
|
|
255
|
+
)
|
|
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
|
+
|
|
264
|
+
**Reply buttons** — up to 3 quick-reply buttons:
|
|
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
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**List** — a button that expands into up to 10 sections of up to 10 rows each:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
Whatsapp::Messages.send_interactive!(
|
|
279
|
+
to: "+15551234567",
|
|
280
|
+
type: :list_buttons,
|
|
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
|
+
)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**CTA URL button** — surfaces a link as a button instead of raw text:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
Whatsapp::Messages.send_interactive!(
|
|
295
|
+
to: "+15551234567",
|
|
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
|
+
```
|
|
301
|
+
|
|
302
|
+
**Media carousel** — 2 to 10 swipeable cards, each with its own header/body/CTA/quick-replies:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
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
|
+
```
|
|
327
|
+
|
|
328
|
+
**Product carousel** — 2 to 10 cards referencing products in your Meta catalog:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
Whatsapp::Messages.send_interactive!(
|
|
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
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
> **Note:** the wire values for the carousel types (`"carousel"` and `"product_list"`) are flagged in the source as worth double-checking against Meta's docs for your specific API version before relying on them in production.
|
|
345
|
+
|
|
346
|
+
### Mark Message As Read
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
Whatsapp::Messages.mark_message_as_read!(message_id: "wamid.HBgLMTU1NTU1NTU1NTUV...")
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Marks an inbound message — and every earlier message in that conversation — as read, powering the "seen" checkmarks on the user's side. Must be called within 30 days of receipt. Unlike every other kind above, this isn't sent through `Messages.new(kind:, payload:).send!`: there's no recipient or `type` envelope, just `message_id:`, so it has its own dedicated method instead of a `kind:` in the factory.
|
|
353
|
+
|
|
354
|
+
## Handling Responses
|
|
355
|
+
|
|
356
|
+
A successful `send!` returns a `Whatsapp::Messages::Response`, built from a payload shaped like:
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"messaging_product": "whatsapp",
|
|
361
|
+
"contacts": [{ "input": "+15551234567", "wa_id": "15551234567" }],
|
|
362
|
+
"messages": [{ "id": "wamid.HBgLMTU1NTU1NTU1NTUV..." }]
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
response = Whatsapp::Messages.send_text!(to: "+15551234567", body: "Hi")
|
|
368
|
+
|
|
369
|
+
response.messages.first.id # => "wamid.HBgLMTU1NTU1NTU1NTUV..."
|
|
370
|
+
response.contacts.first.wa_id # => "15551234567"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Sending raises `ActiveModel::ValidationError` if a field fails local validation, `Whatsapp::Messages::PayloadError` if `kind:` is unrecognized (only reachable through the `Messages.new(kind:, payload:).send!` form), and `Whatsapp::RequestError` if the API responds with a non-2xx status.
|
|
374
|
+
|
|
375
|
+
## Media
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
media = Whatsapp::Media.new
|
|
379
|
+
|
|
380
|
+
media_id = media.upload(file_path: "photo.jpg", type: "image/jpeg")
|
|
381
|
+
info = media.get_url(media_id: media_id) # => { "url" => ..., ... }
|
|
382
|
+
media.download(url: info["url"], save_to: "photo.jpg") # only HTTPS + allowlisted Meta hosts
|
|
383
|
+
media.delete(media_id: media_id) # => true
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`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 attacker-influenced URL.
|
|
388
|
+
|
|
389
|
+
## Managing Templates
|
|
390
|
+
|
|
391
|
+
[Sending a template](#template) requires one that already exists and has been approved by
|
|
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.
|
|
394
|
+
|
|
395
|
+
This is a different API from sending: it addresses your **WhatsApp Business Account**
|
|
396
|
+
(`waba_id`, not `phone_id`) and needs the `whatsapp_business_management` permission.
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
Whatsapp.configure do |config|
|
|
400
|
+
config.api_key = ENV["WHATSAPP_API_KEY"]
|
|
401
|
+
config.waba_id = ENV["WHATSAPP_WABA_ID"]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
templates = Whatsapp::MessageTemplates.new
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Creating
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
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
|
+
)
|
|
426
|
+
|
|
427
|
+
created.id # => "1259544702043867"
|
|
428
|
+
created.status # => "PENDING" — Meta reviews asynchronously, up to 24 hours
|
|
429
|
+
created.pending? # => true
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Named parameters read better than positional ones for anything non-trivial:
|
|
433
|
+
|
|
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
|
+
```
|
|
445
|
+
|
|
446
|
+
Meta's rules are checked before the request, so a mistake raises immediately instead of
|
|
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
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Listing, reading, editing, deleting
|
|
460
|
+
|
|
461
|
+
```ruby
|
|
462
|
+
page = templates.list(status: %w[APPROVED], fields: %w[name category status], limit: 25)
|
|
463
|
+
page.select(&:approved?).map(&:name) # Collection is Enumerable
|
|
464
|
+
page.remaining # headroom against your account's template cap
|
|
465
|
+
templates.list(after: page.next_cursor) if page.next_cursor
|
|
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
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Editing an approved template re-submits it for review but it keeps working meanwhile.
|
|
480
|
+
Approved templates allow 10 edits per 30 days and 1 per 24 hours. Deleting an approved
|
|
481
|
+
template blocks reuse of its name for 30 days.
|
|
482
|
+
|
|
483
|
+
### Other template kinds
|
|
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:
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
templates.upsert(
|
|
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
|
+
```
|
|
498
|
+
|
|
499
|
+
**Marketing carousels** take 2–10 cards that must all share the same structure:
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
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
|
+
)
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Limited-time offers** add a countdown and a coupon code (marketing only; footers are not
|
|
518
|
+
allowed and the body drops to 600 characters):
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
templates.create(
|
|
522
|
+
name: "spring_offer", language: "en_US", category: "MARKETING",
|
|
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:
|
|
538
|
+
|
|
539
|
+
```ruby
|
|
540
|
+
templates.create_from_library(
|
|
541
|
+
name: "my_delivery_update", language: "en_US", category: "UTILITY",
|
|
542
|
+
library_template_name: "delivery_update_1",
|
|
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
|
+
)
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Notes
|
|
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.
|
|
558
|
+
|
|
559
|
+
## Webhooks
|
|
560
|
+
|
|
561
|
+
Meta pushes inbound messages, delivery statuses, and ~18 other account/template
|
|
562
|
+
notification types to a callback URL you register. Inside a Rails app:
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
bundle exec rake whatsapp:install:webhook
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
This copies a personalizable controller to `app/controllers/whatsapp/webhooks_controller.rb`
|
|
569
|
+
and prints the routes and configuration you still need to add by hand:
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
# config/routes.rb
|
|
573
|
+
get "/whatsapp/webhooks", to: "whatsapp/webhooks#verify"
|
|
574
|
+
post "/whatsapp/webhooks", to: "whatsapp/webhooks#receive"
|
|
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
|
+
```
|
|
582
|
+
|
|
583
|
+
The generated controller is yours to edit — it deserializes every notification into typed
|
|
584
|
+
objects and leaves a `# TODO` where your own handling goes:
|
|
585
|
+
|
|
586
|
+
```ruby
|
|
587
|
+
class Whatsapp::WebhooksController < ApplicationController
|
|
588
|
+
def verify
|
|
589
|
+
challenge = Whatsapp::Webhook::Verification.call(params: params)
|
|
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
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
`notification.entry.first.changes.first` gives you a `field` (e.g. `"messages"`) and a typed
|
|
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.
|
|
614
|
+
|
|
615
|
+
**Multi-tenant apps** (many customers, each with their own Meta App) pass `verify_token:`/
|
|
616
|
+
`app_secret:` explicitly instead of relying on the global config default:
|
|
617
|
+
|
|
618
|
+
```ruby
|
|
619
|
+
account = Account.find_by!(slug: params[:account_slug])
|
|
620
|
+
Whatsapp::Webhook::Verification.call(params:, verify_token: account.verify_token)
|
|
621
|
+
Whatsapp::Webhook::Signature.valid?(payload: raw_body, header:, app_secret: account.app_secret)
|
|
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.
|
|
625
|
+
|
|
626
|
+
## Development
|
|
627
|
+
|
|
628
|
+
After checking out the repo, run `bundle install`, then:
|
|
629
|
+
|
|
630
|
+
```bash
|
|
631
|
+
bundle exec rake # run the specs and RuboCop (the default task)
|
|
632
|
+
bundle exec rspec # specs only
|
|
633
|
+
bundle exec rubocop # lint only
|
|
634
|
+
bin/console # interactive prompt
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Contributing
|
|
638
|
+
|
|
639
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/saleszera/ruby-whatsapp.
|
|
640
|
+
|
|
641
|
+
## License
|
|
642
|
+
|
|
643
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|