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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/README.md +322 -617
  4. data/docs/README.md +76 -0
  5. data/docs/business_phone_number/README.md +243 -0
  6. data/docs/business_phone_number/account.md +170 -0
  7. data/docs/business_phone_number/contact_book.md +90 -0
  8. data/docs/business_phone_number/profile.md +220 -0
  9. data/docs/business_phone_number/username.md +157 -0
  10. data/docs/configuration.md +177 -0
  11. data/docs/errors.md +219 -0
  12. data/docs/media/README.md +161 -0
  13. data/docs/message_templates/README.md +206 -0
  14. data/docs/message_templates/authentication.md +107 -0
  15. data/docs/message_templates/carousel.md +103 -0
  16. data/docs/message_templates/components.md +215 -0
  17. data/docs/message_templates/library.md +98 -0
  18. data/docs/message_templates/limited_time_offer.md +94 -0
  19. data/docs/message_templates/responses.md +173 -0
  20. data/docs/message_templates/standard.md +138 -0
  21. data/docs/messages/README.md +212 -0
  22. data/docs/messages/address.md +86 -0
  23. data/docs/messages/audio.md +43 -0
  24. data/docs/messages/contacts.md +129 -0
  25. data/docs/messages/document.md +49 -0
  26. data/docs/messages/image.md +59 -0
  27. data/docs/messages/interactive.md +342 -0
  28. data/docs/messages/location.md +51 -0
  29. data/docs/messages/location_request.md +59 -0
  30. data/docs/messages/mark_message_as_read.md +84 -0
  31. data/docs/messages/reaction.md +59 -0
  32. data/docs/messages/sticker.md +40 -0
  33. data/docs/messages/template.md +193 -0
  34. data/docs/messages/text.md +55 -0
  35. data/docs/messages/video.md +41 -0
  36. data/docs/schedule/README.md +183 -0
  37. data/docs/subscribed_app/README.md +139 -0
  38. data/docs/webhooks/README.md +217 -0
  39. data/docs/webhooks/account_alerts.md +44 -0
  40. data/docs/webhooks/account_review_update.md +29 -0
  41. data/docs/webhooks/account_update.md +54 -0
  42. data/docs/webhooks/automatic_events.md +47 -0
  43. data/docs/webhooks/business_capability_update.md +40 -0
  44. data/docs/webhooks/business_username_updates.md +55 -0
  45. data/docs/webhooks/history.md +107 -0
  46. data/docs/webhooks/message_template_components_update.md +46 -0
  47. data/docs/webhooks/message_template_quality_update.md +49 -0
  48. data/docs/webhooks/message_template_status_update.md +55 -0
  49. data/docs/webhooks/messages.md +520 -0
  50. data/docs/webhooks/partner_solutions.md +35 -0
  51. data/docs/webhooks/payment_configuration_update.md +40 -0
  52. data/docs/webhooks/phone_number_name_update.md +44 -0
  53. data/docs/webhooks/phone_number_quality_update.md +40 -0
  54. data/docs/webhooks/security.md +43 -0
  55. data/docs/webhooks/smb_app_state_sync.md +91 -0
  56. data/docs/webhooks/smb_message_echoes.md +91 -0
  57. data/docs/webhooks/template_category_update.md +52 -0
  58. data/docs/webhooks/user_id_update.md +84 -0
  59. data/docs/webhooks/user_preferences.md +87 -0
  60. data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
  61. data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
  62. data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
  63. data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
  64. data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
  65. data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
  66. data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
  67. data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
  68. data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
  69. data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
  70. data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
  71. data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
  72. data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
  73. data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
  74. data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
  75. data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
  76. data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
  77. data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
  78. data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
  79. data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
  80. data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
  81. data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
  82. data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
  83. data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
  84. data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
  85. data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
  86. data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
  87. data/lib/ruby/whatsapp/message_templates.rb +2 -6
  88. data/lib/ruby/whatsapp/messages/base.rb +47 -5
  89. data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
  90. data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
  91. data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
  92. data/lib/ruby/whatsapp/path_building.rb +33 -0
  93. data/lib/ruby/whatsapp/schedule/create.rb +101 -0
  94. data/lib/ruby/whatsapp/schedule/list.rb +50 -0
  95. data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
  96. data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
  97. data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
  98. data/lib/ruby/whatsapp/schedule/response.rb +18 -0
  99. data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
  100. data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
  101. data/lib/ruby/whatsapp/schedule.rb +15 -0
  102. data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
  103. data/lib/ruby/whatsapp/version.rb +1 -1
  104. data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
  105. data/lib/ruby/whatsapp/webhook/change.rb +3 -1
  106. data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
  107. data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
  108. data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
  109. data/lib/ruby/whatsapp/webhook/history.rb +73 -34
  110. data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
  111. data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
  112. data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
  113. data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
  114. data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
  115. data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
  116. data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
  117. data/lib/ruby/whatsapp/webhook/message.rb +3 -0
  118. data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
  119. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
  120. data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
  121. data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
  122. data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
  123. data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
  124. data/lib/ruby/whatsapp/webhook/status.rb +23 -2
  125. data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
  126. data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
  127. data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
  128. data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
  129. data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
  130. 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>