ruby-whatsapp 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -0
- data/README.md +322 -617
- data/docs/README.md +76 -0
- data/docs/business_phone_number/README.md +243 -0
- data/docs/business_phone_number/account.md +170 -0
- data/docs/business_phone_number/contact_book.md +90 -0
- data/docs/business_phone_number/profile.md +220 -0
- data/docs/business_phone_number/username.md +157 -0
- data/docs/configuration.md +177 -0
- data/docs/errors.md +219 -0
- data/docs/media/README.md +161 -0
- data/docs/message_templates/README.md +206 -0
- data/docs/message_templates/authentication.md +107 -0
- data/docs/message_templates/carousel.md +103 -0
- data/docs/message_templates/components.md +215 -0
- data/docs/message_templates/library.md +98 -0
- data/docs/message_templates/limited_time_offer.md +94 -0
- data/docs/message_templates/responses.md +173 -0
- data/docs/message_templates/standard.md +138 -0
- data/docs/messages/README.md +212 -0
- data/docs/messages/address.md +86 -0
- data/docs/messages/audio.md +43 -0
- data/docs/messages/contacts.md +129 -0
- data/docs/messages/document.md +49 -0
- data/docs/messages/image.md +59 -0
- data/docs/messages/interactive.md +342 -0
- data/docs/messages/location.md +51 -0
- data/docs/messages/location_request.md +59 -0
- data/docs/messages/mark_message_as_read.md +84 -0
- data/docs/messages/reaction.md +59 -0
- data/docs/messages/sticker.md +40 -0
- data/docs/messages/template.md +193 -0
- data/docs/messages/text.md +55 -0
- data/docs/messages/video.md +41 -0
- data/docs/schedule/README.md +183 -0
- data/docs/subscribed_app/README.md +139 -0
- data/docs/webhooks/README.md +217 -0
- data/docs/webhooks/account_alerts.md +44 -0
- data/docs/webhooks/account_review_update.md +29 -0
- data/docs/webhooks/account_update.md +54 -0
- data/docs/webhooks/automatic_events.md +47 -0
- data/docs/webhooks/business_capability_update.md +40 -0
- data/docs/webhooks/business_username_updates.md +55 -0
- data/docs/webhooks/history.md +107 -0
- data/docs/webhooks/message_template_components_update.md +46 -0
- data/docs/webhooks/message_template_quality_update.md +49 -0
- data/docs/webhooks/message_template_status_update.md +55 -0
- data/docs/webhooks/messages.md +520 -0
- data/docs/webhooks/partner_solutions.md +35 -0
- data/docs/webhooks/payment_configuration_update.md +40 -0
- data/docs/webhooks/phone_number_name_update.md +44 -0
- data/docs/webhooks/phone_number_quality_update.md +40 -0
- data/docs/webhooks/security.md +43 -0
- data/docs/webhooks/smb_app_state_sync.md +91 -0
- data/docs/webhooks/smb_message_echoes.md +91 -0
- data/docs/webhooks/template_category_update.md +52 -0
- data/docs/webhooks/user_id_update.md +84 -0
- data/docs/webhooks/user_preferences.md +87 -0
- data/lib/ruby/whatsapp/business_phone_number/account/details.rb +149 -0
- data/lib/ruby/whatsapp/business_phone_number/account/get.rb +53 -0
- data/lib/ruby/whatsapp/business_phone_number/account/transport.rb +30 -0
- data/lib/ruby/whatsapp/business_phone_number/account/update.rb +76 -0
- data/lib/ruby/whatsapp/business_phone_number/account.rb +23 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/delete.rb +87 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book/deletion.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/contact_book.rb +25 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/details.rb +105 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/get.rb +56 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/update.rb +148 -0
- data/lib/ruby/whatsapp/business_phone_number/profile/verticals.rb +54 -0
- data/lib/ruby/whatsapp/business_phone_number/profile.rb +22 -0
- data/lib/ruby/whatsapp/business_phone_number/response.rb +3 -3
- data/lib/ruby/whatsapp/business_phone_number/transport.rb +12 -12
- data/lib/ruby/whatsapp/business_phone_number/username/claim.rb +180 -0
- data/lib/ruby/whatsapp/business_phone_number/username/delete.rb +36 -0
- data/lib/ruby/whatsapp/business_phone_number/username/details.rb +66 -0
- data/lib/ruby/whatsapp/business_phone_number/username/get.rb +33 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggest.rb +37 -0
- data/lib/ruby/whatsapp/business_phone_number/username/suggestions.rb +50 -0
- data/lib/ruby/whatsapp/business_phone_number/username.rb +27 -0
- data/lib/ruby/whatsapp/business_phone_number.rb +20 -4
- data/lib/ruby/whatsapp/message_templates/button/request_contact_info.rb +66 -0
- data/lib/ruby/whatsapp/message_templates/button.rb +5 -2
- data/lib/ruby/whatsapp/message_templates/component_set.rb +28 -0
- data/lib/ruby/whatsapp/message_templates/response/paging.rb +2 -1
- data/lib/ruby/whatsapp/message_templates.rb +2 -6
- data/lib/ruby/whatsapp/messages/base.rb +47 -5
- data/lib/ruby/whatsapp/messages/interactive/request_contact_info.rb +59 -0
- data/lib/ruby/whatsapp/messages/interactive.rb +22 -4
- data/lib/ruby/whatsapp/messages/response/contacts.rb +22 -5
- data/lib/ruby/whatsapp/path_building.rb +33 -0
- data/lib/ruby/whatsapp/schedule/create.rb +101 -0
- data/lib/ruby/whatsapp/schedule/list.rb +50 -0
- data/lib/ruby/whatsapp/schedule/response/collection.rb +61 -0
- data/lib/ruby/whatsapp/schedule/response/created.rb +30 -0
- data/lib/ruby/whatsapp/schedule/response/node.rb +85 -0
- data/lib/ruby/whatsapp/schedule/response.rb +18 -0
- data/lib/ruby/whatsapp/schedule/statuses.rb +19 -0
- data/lib/ruby/whatsapp/schedule/transport.rb +24 -0
- data/lib/ruby/whatsapp/schedule.rb +15 -0
- data/lib/ruby/whatsapp/subscribed_app/transport.rb +6 -5
- data/lib/ruby/whatsapp/version.rb +1 -1
- data/lib/ruby/whatsapp/webhook/business_username_updates.rb +51 -0
- data/lib/ruby/whatsapp/webhook/change.rb +3 -1
- data/lib/ruby/whatsapp/webhook/contact.rb +26 -3
- data/lib/ruby/whatsapp/webhook/history/chunk.rb +50 -0
- data/lib/ruby/whatsapp/webhook/history/thread.rb +64 -0
- data/lib/ruby/whatsapp/webhook/history.rb +73 -34
- data/lib/ruby/whatsapp/webhook/message/base.rb +27 -2
- data/lib/ruby/whatsapp/webhook/message/contacts/contact.rb +15 -2
- data/lib/ruby/whatsapp/webhook/message/context.rb +23 -2
- data/lib/ruby/whatsapp/webhook/message/edit.rb +50 -0
- data/lib/ruby/whatsapp/webhook/message/revoke.rb +36 -0
- data/lib/ruby/whatsapp/webhook/message/system.rb +19 -2
- data/lib/ruby/whatsapp/webhook/message/unknown.rb +2 -2
- data/lib/ruby/whatsapp/webhook/message.rb +3 -0
- data/lib/ruby/whatsapp/webhook/messages.rb +4 -3
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/contact.rb +76 -0
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync/state_sync.rb +18 -4
- data/lib/ruby/whatsapp/webhook/smb_app_state_sync.rb +33 -9
- data/lib/ruby/whatsapp/webhook/smb_message_echoes/echo.rb +60 -0
- data/lib/ruby/whatsapp/webhook/smb_message_echoes.rb +26 -11
- data/lib/ruby/whatsapp/webhook/status.rb +23 -2
- data/lib/ruby/whatsapp/webhook/user_id_update/entry.rb +56 -0
- data/lib/ruby/whatsapp/webhook/user_id_update/id_change.rb +35 -0
- data/lib/ruby/whatsapp/webhook/user_id_update.rb +61 -0
- data/lib/ruby/whatsapp/webhook/user_preferences/entry.rb +76 -0
- data/lib/ruby/whatsapp/webhook/user_preferences.rb +44 -23
- metadata +100 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Carousel Templates
|
|
2
|
+
|
|
3
|
+
Two to ten swipeable cards under a shared body message — a product row, a set of
|
|
4
|
+
listings, a group of offers.
|
|
5
|
+
|
|
6
|
+
> **MARKETING only.** A carousel on a `UTILITY` or `AUTHENTICATION` template is
|
|
7
|
+
> rejected locally:
|
|
8
|
+
>
|
|
9
|
+
> ```ruby
|
|
10
|
+
> # => ActiveModel::ValidationError: Components CAROUSEL is only supported on
|
|
11
|
+
> # MARKETING templates, not UTILITY
|
|
12
|
+
> ```
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
card = {
|
|
16
|
+
header: { format: "IMAGE", header_handle: "4::aW..." },
|
|
17
|
+
body: { text: "Rare {{1}} in stock!", example: ["Tulips"] },
|
|
18
|
+
buttons: [{ type: :quick_reply, text: "More like this" }],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Whatsapp::MessageTemplates.new.create(
|
|
22
|
+
name: "summer_carousel", language: "en_US", category: "MARKETING",
|
|
23
|
+
components: [
|
|
24
|
+
{ type: :body, text: "Summer is here, {{1}}!", example: ["Pablo"] },
|
|
25
|
+
{ type: :carousel, cards: [card, card] },
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The template still needs its own top-level `BODY` — the carousel sits *below* it.
|
|
31
|
+
|
|
32
|
+
## Card structure
|
|
33
|
+
|
|
34
|
+
| Field | Required | Rules |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `header` | yes | `format` must be `IMAGE` or `VIDEO` |
|
|
37
|
+
| `body` | no | Same rules as a normal body |
|
|
38
|
+
| `buttons` | no | **0–2** buttons per card |
|
|
39
|
+
|
|
40
|
+
> **All cards must be structurally identical.** Same header format, same button types
|
|
41
|
+
> in the same order, and body text on either every card or none. The gem computes a
|
|
42
|
+
> signature per card and compares them:
|
|
43
|
+
>
|
|
44
|
+
> ```ruby
|
|
45
|
+
> # => ActiveModel::ValidationError: Cards must all have an identical structure:
|
|
46
|
+
> # same header format, same button types, and body text on either every card or none
|
|
47
|
+
> ```
|
|
48
|
+
|
|
49
|
+
Meta says "up to 2" buttons and never states a minimum, so **zero buttons is valid**.
|
|
50
|
+
|
|
51
|
+
## Serialized payload
|
|
52
|
+
|
|
53
|
+
Each card serializes to a bare `{components: [...]}` — note there is **no
|
|
54
|
+
`card_index`** here. That is send-side only:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Whatsapp::MessageTemplates::Template.new(
|
|
58
|
+
name: "summer_carousel", language: "en_US", category: "marketing",
|
|
59
|
+
components: [
|
|
60
|
+
{ type: :body, text: "Summer is here, {{1}}!", example: ["Pablo"] },
|
|
61
|
+
{ type: :carousel, cards: [card, card] },
|
|
62
|
+
]
|
|
63
|
+
).serialize
|
|
64
|
+
# => {
|
|
65
|
+
# name: "summer_carousel", language: "en_US", category: "MARKETING",
|
|
66
|
+
# parameter_format: "POSITIONAL",
|
|
67
|
+
# components: [
|
|
68
|
+
# { type: "BODY", text: "Summer is here, {{1}}!", example: { body_text: [["Pablo"]] } },
|
|
69
|
+
# { type: "CAROUSEL", cards: [
|
|
70
|
+
# { components: [
|
|
71
|
+
# { type: "HEADER", format: "IMAGE", example: { header_handle: ["4::aW..."] } },
|
|
72
|
+
# { type: "BODY", text: "Rare {{1}} in stock!", example: { body_text: [["Tulips"]] } },
|
|
73
|
+
# { type: "BUTTONS", buttons: [{ type: "QUICK_REPLY", text: "More like this" }] },
|
|
74
|
+
# ] },
|
|
75
|
+
# { components: [...] },
|
|
76
|
+
# ] },
|
|
77
|
+
# ]
|
|
78
|
+
# }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Media handles
|
|
82
|
+
|
|
83
|
+
`header_handle` is **not** a media ID from [`Media#upload`](../media/README.md). It
|
|
84
|
+
comes from Meta's Resumable Upload API, which this gem does not wrap — see
|
|
85
|
+
[README.md § Not wrapped](README.md#not-wrapped).
|
|
86
|
+
|
|
87
|
+
## Validation errors
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
{ type: :carousel, cards: [card] }
|
|
91
|
+
# => ActiveModel::ValidationError: Cards is too short (minimum is 2 characters)
|
|
92
|
+
|
|
93
|
+
{ type: :carousel, cards: [card_with_image_header, card_with_video_header] }
|
|
94
|
+
# => ActiveModel::ValidationError: Cards must all have an identical structure: ...
|
|
95
|
+
|
|
96
|
+
{ header: { format: "TEXT", text: "Hi" }, body: ..., buttons: [...] }
|
|
97
|
+
# => ActiveModel::ValidationError: Header format must be IMAGE or VIDEO
|
|
98
|
+
# for a carousel card, got TEXT
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
**Meta docs:** <https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates/carousel-templates>
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Components and Buttons Reference
|
|
2
|
+
|
|
3
|
+
The complete registry of what a template can be built from. A component validates
|
|
4
|
+
itself; `ComponentSet` validates how components relate to each other.
|
|
5
|
+
|
|
6
|
+
## Component types
|
|
7
|
+
|
|
8
|
+
Resolved through the frozen `Component::TYPES` registry — never `const_get` on caller
|
|
9
|
+
input.
|
|
10
|
+
|
|
11
|
+
| `type:` | Wire type | Purpose |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `:header` | `HEADER` | Text, media, or location above the body |
|
|
14
|
+
| `:body` | `BODY` | **Required, exactly one** |
|
|
15
|
+
| `:footer` | `FOOTER` | Small print below the body |
|
|
16
|
+
| `:buttons` | `BUTTONS` | One component holding 1–10 buttons |
|
|
17
|
+
| `:carousel` | `CAROUSEL` | [2–10 identical cards](carousel.md) |
|
|
18
|
+
| `:limited_time_offer` | `LIMITED_TIME_OFFER` | [Countdown + coupon](limited_time_offer.md) |
|
|
19
|
+
|
|
20
|
+
An unknown type raises before any request:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# => Whatsapp::MessageTemplates::TemplateError: Unknown component type: :greeting.
|
|
24
|
+
# Known types: header, body, footer, buttons, carousel, limited_time_offer
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Header
|
|
28
|
+
|
|
29
|
+
| `format` | Fields | Rules |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `TEXT` | `text`, `example` | ≤ **60** chars, **at most one** placeholder, no Markdown, no `header_handle` |
|
|
32
|
+
| `IMAGE` `VIDEO` `DOCUMENT` `GIF` | `header_handle` | No `text` |
|
|
33
|
+
| `LOCATION` | — | Nothing else; coordinates are supplied when sending |
|
|
34
|
+
|
|
35
|
+
Max **one** header per template.
|
|
36
|
+
|
|
37
|
+
The Markdown check rejects `*` `_` `~` `` ` `` in the literal text but **ignores
|
|
38
|
+
placeholder contents** — named placeholders are lowercase-and-underscores by
|
|
39
|
+
definition, so scanning raw text would reject `{{sale_start_date}}` for its underscore
|
|
40
|
+
and make named parameters unusable in a text header.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
{ type: :header, format: "TEXT", text: "Order {{1}} confirmed", example: ["#1234"] }
|
|
44
|
+
{ type: :header, format: "IMAGE", header_handle: "4::aW..." }
|
|
45
|
+
{ type: :header, format: "LOCATION" }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A `LOCATION` header requires the category to be `UTILITY` or `MARKETING`.
|
|
49
|
+
|
|
50
|
+
### Body
|
|
51
|
+
|
|
52
|
+
**Required, exactly one per template.** Two mutually exclusive shapes:
|
|
53
|
+
|
|
54
|
+
| Shape | Fields | Rules |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| Text | `text`, `example` | ≤ **1024** chars, any number of placeholders, example count must match |
|
|
57
|
+
| Authentication | `add_security_recommendation` | See [authentication.md](authentication.md) |
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
{ type: :body, text: "Thank you, {{1}}!", example: ["Pablo"] }
|
|
61
|
+
{ type: :body, add_security_recommendation: true }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Example count is checked against the **unique** placeholder count, so a repeated
|
|
65
|
+
`{{1}}` needs one example, not two.
|
|
66
|
+
|
|
67
|
+
### Footer
|
|
68
|
+
|
|
69
|
+
Max **one**. Two mutually exclusive shapes:
|
|
70
|
+
|
|
71
|
+
| Shape | Fields | Rules |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| Text | `text` | ≤ **60** chars, **no placeholders at all** |
|
|
74
|
+
| Authentication | `code_expiration_minutes` | Integer in **1..90** |
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
{ type: :footer, text: "Thanks for shopping with us" }
|
|
78
|
+
{ type: :footer, code_expiration_minutes: 15 }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Forbidden entirely alongside a [limited-time offer](limited_time_offer.md).
|
|
82
|
+
|
|
83
|
+
### Buttons
|
|
84
|
+
|
|
85
|
+
Max **one** `BUTTONS` component, holding **1–10** buttons.
|
|
86
|
+
|
|
87
|
+
| Button type | Cap per template |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `QUICK_REPLY` | 10 |
|
|
90
|
+
| `URL` | 2 |
|
|
91
|
+
| `PHONE_NUMBER` | 1 |
|
|
92
|
+
| `COPY_CODE` | 1 |
|
|
93
|
+
| `OTP` | uncapped — Meta publishes no limit |
|
|
94
|
+
|
|
95
|
+
> **Quick replies must be contiguous.** They may sit at the start or the end, but not
|
|
96
|
+
> with another type between them. `[QR, QR, URL]` and `[URL, QR, QR]` are fine;
|
|
97
|
+
> `[QR, URL, QR]` raises `quick reply buttons must be grouped together, without other
|
|
98
|
+
> types between them`.
|
|
99
|
+
|
|
100
|
+
## Button types
|
|
101
|
+
|
|
102
|
+
Resolved through the frozen `Button::TYPES` registry.
|
|
103
|
+
|
|
104
|
+
| `type:` | Wire type | Fields |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `:quick_reply` | `QUICK_REPLY` | `text` ≤ **25** |
|
|
107
|
+
| `:url` | `URL` | `text` ≤ 25, `url` ≤ **2000**, `example` |
|
|
108
|
+
| `:phone_number` | `PHONE_NUMBER` | `text` ≤ 25, `phone_number` ≤ **20** |
|
|
109
|
+
| `:copy_code` | `COPY_CODE` | `example` ≤ **20** (a bare String), **no `text`** |
|
|
110
|
+
| `:otp` | `OTP` | `otp_type`, `supported_apps`, `zero_tap_terms_accepted` |
|
|
111
|
+
| `:request_contact_info` | `REQUEST_CONTACT_INFO` | **no fields** — `text` optional and only ever `"Share Contact Info"` |
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
{ type: :quick_reply, text: "More like this" }
|
|
115
|
+
{ type: :url, text: "Track order", url: "https://example.com/o/{{1}}", example: "1234" }
|
|
116
|
+
{ type: :phone_number, text: "Call", phone_number: "15550051310" }
|
|
117
|
+
{ type: :copy_code, example: "SPRING25" }
|
|
118
|
+
{ type: :otp, otp_type: "COPY_CODE" }
|
|
119
|
+
{ type: :request_contact_info }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### The URL button's single trailing variable
|
|
123
|
+
|
|
124
|
+
A URL button may contain **at most one variable, and it must be at the end**:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
{ type: :url, text: "Track", url: "https://example.com/{{1}}/details" }
|
|
128
|
+
# => ActiveModel::ValidationError: Url variable must be at the end of the URL
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> The URL variable is counted by **occurrence**, not by unique name, so
|
|
132
|
+
> `.../{{1}}/details/{{1}}` is rejected: it ends in a placeholder and names one
|
|
133
|
+
> parameter, but the leading one still sits mid-URL. Everywhere else — body examples
|
|
134
|
+
> above all — the unique count is the correct one, since Meta treats a repeated
|
|
135
|
+
> `{{1}}` as a single parameter needing a single example.
|
|
136
|
+
|
|
137
|
+
Its `example` is a **flat array on the button**, unlike header and body examples,
|
|
138
|
+
which are nested under an `example` key. A bare String is wrapped for you.
|
|
139
|
+
|
|
140
|
+
### Buttons Meta supplies the label for
|
|
141
|
+
|
|
142
|
+
`COPY_CODE` and `OTP` reject `text` (and `autofill_text`) — Meta writes and localises
|
|
143
|
+
those labels:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
{ type: :copy_code, example: "SPRING25", text: "Copy" }
|
|
147
|
+
# => ActiveModel::ValidationError: Text cannot be set on a copy-code button;
|
|
148
|
+
# Meta supplies the label
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### The request-contact-info button
|
|
152
|
+
|
|
153
|
+
Asks the recipient to share their phone number, which they hand over with one tap. It
|
|
154
|
+
takes nothing:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
{ type: :buttons, buttons: [{ type: :request_contact_info }] }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This is the counterpart to Meta's usernames rollout: once a user adopts a username their
|
|
161
|
+
phone number may stop reaching you, leaving only their
|
|
162
|
+
[BSUID](../webhooks/messages.md#business-scoped-user-ids-bsuid). It is also the only way
|
|
163
|
+
to reach such a user with a one-tap, zero-tap, or copy-code
|
|
164
|
+
[authentication template](authentication.md), since those require a phone number.
|
|
165
|
+
|
|
166
|
+
Three things about it:
|
|
167
|
+
|
|
168
|
+
- **The label is not customizable.** Meta renders it in the recipient's own app language.
|
|
169
|
+
A `text` field is accepted but must be exactly `"Share Contact Info"`, so the simplest
|
|
170
|
+
correct thing is to omit it.
|
|
171
|
+
- **`UTILITY` and `MARKETING` only** — a cross-component rule, listed below.
|
|
172
|
+
- **Nothing to fill in when sending.** Unlike a URL or copy-code button, it takes no
|
|
173
|
+
parameter values, so a template carrying one needs no `button` component in the
|
|
174
|
+
[send payload](../messages/template.md) at all.
|
|
175
|
+
|
|
176
|
+
A tap arrives as an inbound [`contacts` message](../webhooks/messages.md#contacts) whose
|
|
177
|
+
`origin` is `"contact_request"`. There is also an
|
|
178
|
+
[interactive-message form](../messages/interactive.md) of the same button, for use inside
|
|
179
|
+
the 24-hour service window.
|
|
180
|
+
|
|
181
|
+
No per-type cap is enforced: Meta publishes none, and inventing one would reject valid
|
|
182
|
+
payloads — the same reason `OTP` is uncapped. The overall 10-button limit still applies.
|
|
183
|
+
|
|
184
|
+
## Cross-component rules
|
|
185
|
+
|
|
186
|
+
Owned by `ComponentSet`, because it is the only object that can see the siblings:
|
|
187
|
+
|
|
188
|
+
- **Exactly one** `BODY`.
|
|
189
|
+
- **At most one** each of `HEADER`, `FOOTER`, `BUTTONS`, `CAROUSEL`,
|
|
190
|
+
`LIMITED_TIME_OFFER`.
|
|
191
|
+
- `CAROUSEL` or `LIMITED_TIME_OFFER` → category must be `MARKETING`.
|
|
192
|
+
- A `LOCATION` header → category must be `UTILITY` or `MARKETING`.
|
|
193
|
+
- A `REQUEST_CONTACT_INFO` button → category must be `UTILITY` or `MARKETING`.
|
|
194
|
+
- `LIMITED_TIME_OFFER` present → `FOOTER` forbidden, `BODY` ≤ **600**, header
|
|
195
|
+
`IMAGE`/`VIDEO` only, copy-code `example` ≤ **15** and that button must be **first**.
|
|
196
|
+
|
|
197
|
+
The category-dependent rules are **skipped when `category` is nil** — which is exactly
|
|
198
|
+
what `#update` needs, since editing replaces components without re-supplying a
|
|
199
|
+
category.
|
|
200
|
+
|
|
201
|
+
## Not registered
|
|
202
|
+
|
|
203
|
+
Only types with a published Meta field reference get a class. Named in Meta's Graph
|
|
204
|
+
enum but deliberately absent:
|
|
205
|
+
|
|
206
|
+
- **Components:** `GREETING`, `ALBUM`, `CALL_PERMISSION_REQUEST`,
|
|
207
|
+
`TAP_TARGET_CONFIGURATION`, `ATTACHMENT`.
|
|
208
|
+
- **Buttons:** `FLOW`, `MPM`, `CATALOG`, `VOICE_CALL`, `VIDEO_CALL`, `POSTBACK`,
|
|
209
|
+
`BOOKING_STATUS`, `PAYMENT_REQUEST`. Most of these *are*
|
|
210
|
+
accepted by [`LibraryTemplate`](library.md), because Meta documents them by name in
|
|
211
|
+
that context.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
**Meta docs:** <https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates/components/>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Library Templates
|
|
2
|
+
|
|
3
|
+
Meta maintains a library of pre-written, pre-approved templates for common cases —
|
|
4
|
+
delivery updates, appointment reminders, payment confirmations. Cloning one usually
|
|
5
|
+
comes back **`APPROVED` immediately**, skipping the 24-hour review entirely.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
Whatsapp::MessageTemplates.new.create_from_library(
|
|
9
|
+
name: "my_delivery_update", language: "en_US", category: "UTILITY",
|
|
10
|
+
library_template_name: "delivery_update_1",
|
|
11
|
+
library_template_button_inputs: [
|
|
12
|
+
{ type: "URL", url: { base_url: "https://example.com/{{1}}",
|
|
13
|
+
url_suffix_example: "https://example.com/order_update" } },
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
# => #<Response::Created id="1" status="APPROVED" category="UTILITY">
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Fields
|
|
20
|
+
|
|
21
|
+
| Field | Required | Rules |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `name` | yes | Your name for the clone. `/\A[a-z0-9_]+\z/`, max 512 |
|
|
24
|
+
| `language` | yes | Validated against the supported list |
|
|
25
|
+
| `category` | yes | `UTILITY` \| `MARKETING` \| `AUTHENTICATION` |
|
|
26
|
+
| `library_template_name` | yes | Meta's name for the source template |
|
|
27
|
+
| `library_template_body_inputs` | no | Body toggles — see below |
|
|
28
|
+
| `library_template_button_inputs` | no | Button configuration — see below |
|
|
29
|
+
|
|
30
|
+
> **No `components:` at all.** The wording is Meta's; you only supply the identity and
|
|
31
|
+
> the few configurable inputs. That is the whole point of the library.
|
|
32
|
+
|
|
33
|
+
## Body inputs
|
|
34
|
+
|
|
35
|
+
| Field | Type |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `add_contact_number` | Boolean |
|
|
38
|
+
| `add_learn_more_link` | Boolean |
|
|
39
|
+
| `add_security_recommendation` | Boolean |
|
|
40
|
+
| `add_track_package_link` | Boolean |
|
|
41
|
+
| `code_expiration_minutes` | Integer, **1–90** |
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
library_template_body_inputs: {
|
|
45
|
+
add_contact_number: true,
|
|
46
|
+
add_track_package_link: true,
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Button inputs
|
|
51
|
+
|
|
52
|
+
`type` is one of `QUICK_REPLY` `URL` `PHONE_NUMBER` `OTP` `MPM` `CATALOG` `FLOW`
|
|
53
|
+
`VOICE_CALL` `APP` — deliberately a **wider set** than the button types available to
|
|
54
|
+
hand-built templates, because Meta documents these by name in the library context.
|
|
55
|
+
|
|
56
|
+
| `type` | Required extra |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `URL` | `url: { base_url:, url_suffix_example: }` |
|
|
59
|
+
| `PHONE_NUMBER` | `phone_number:` |
|
|
60
|
+
| `APP` | `supported_apps:` |
|
|
61
|
+
| `OTP` | `otp_type:`, plus `zero_tap_terms_accepted:` for zero-tap |
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
library_template_button_inputs: [
|
|
65
|
+
{ type: "URL", url: { base_url: "https://example.com/{{1}}",
|
|
66
|
+
url_suffix_example: "https://example.com/order_update" } },
|
|
67
|
+
{ type: "PHONE_NUMBER", phone_number: "15550051310" },
|
|
68
|
+
]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Validation errors
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
{ type: "URL", url: {} }
|
|
75
|
+
# => ActiveModel::ValidationError: Url requires a base_url for a URL button input
|
|
76
|
+
|
|
77
|
+
{ type: "PHONE_NUMBER" }
|
|
78
|
+
# => ActiveModel::ValidationError: Phone number can't be blank for a PHONE_NUMBER button input
|
|
79
|
+
|
|
80
|
+
{ type: "APP" }
|
|
81
|
+
# => ActiveModel::ValidationError: Supported apps can't be blank for an APP button input
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Unverified detail
|
|
85
|
+
|
|
86
|
+
`library_template_button_inputs` is emitted as a **real JSON array**. Meta's guide
|
|
87
|
+
shows a JSON-*stringified* array while the Graph reference types it
|
|
88
|
+
`array<JSON object>`. This has not been settled against a live WABA — if Meta rejects
|
|
89
|
+
the array form, it is a one-line change in `LibraryTemplate#serialize`.
|
|
90
|
+
|
|
91
|
+
## Not wrapped
|
|
92
|
+
|
|
93
|
+
Browsing the library itself (`message_template_library`) is not wrapped — find the
|
|
94
|
+
`library_template_name` you want in WhatsApp Manager or Meta's docs.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-library>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Limited-Time Offer Templates
|
|
2
|
+
|
|
3
|
+
Adds a live countdown and a copyable coupon code to a marketing message. The countdown
|
|
4
|
+
is rendered by WhatsApp itself, so the urgency stays accurate no matter when the
|
|
5
|
+
recipient opens the thread.
|
|
6
|
+
|
|
7
|
+
> **MARKETING only**, and the presence of a `LIMITED_TIME_OFFER` component tightens
|
|
8
|
+
> four other rules at once. See [The knock-on rules](#the-knock-on-rules).
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
Whatsapp::MessageTemplates.new.create(
|
|
12
|
+
name: "spring_offer", language: "en_US", category: "MARKETING",
|
|
13
|
+
components: [
|
|
14
|
+
{ type: :header, format: "IMAGE", header_handle: "4::aW..." },
|
|
15
|
+
{ type: :limited_time_offer, text: "Expiring offer!", has_expiration: true },
|
|
16
|
+
{ type: :body, text: "Good news, {{1}}! Use code {{2}} for 25% off.",
|
|
17
|
+
example: ["Pablo", "SPRING25"] },
|
|
18
|
+
{ type: :buttons, buttons: [
|
|
19
|
+
{ type: :copy_code, example: "SPRING25" },
|
|
20
|
+
{ type: :url, text: "Book now!", url: "https://example.com/o?c={{1}}", example: "n3mtql" },
|
|
21
|
+
] },
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The component
|
|
27
|
+
|
|
28
|
+
| Field | Required | Rules |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `text` | yes | Max **16** characters — the offer label |
|
|
31
|
+
| `has_expiration` | no | `true` shows the countdown |
|
|
32
|
+
|
|
33
|
+
It serializes nested, unlike every other component:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
{ type: "LIMITED_TIME_OFFER", limited_time_offer: { text: "Expiring offer!", has_expiration: true } }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## The knock-on rules
|
|
40
|
+
|
|
41
|
+
Adding a limited-time offer changes what the rest of the template may contain. These
|
|
42
|
+
are enforced by `ComponentSet`, which is the only object that can see all the
|
|
43
|
+
components at once:
|
|
44
|
+
|
|
45
|
+
| Rule | Error if violated |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| **No `FOOTER`** at all | `a FOOTER is not allowed in a LIMITED_TIME_OFFER template` |
|
|
48
|
+
| `BODY` drops to **600** chars (from 1024) | `the BODY of a LIMITED_TIME_OFFER template is limited to 600 characters` |
|
|
49
|
+
| Header must be **`IMAGE` or `VIDEO`** | `the header must be IMAGE or VIDEO in a LIMITED_TIME_OFFER template, got TEXT` |
|
|
50
|
+
| Copy-code button must be **first** | `the copy-code button must be first in a LIMITED_TIME_OFFER template` |
|
|
51
|
+
| Copy-code `example` ≤ **15** chars (from 20) | `the copy-code example is limited to 15 characters in a LIMITED_TIME_OFFER template` |
|
|
52
|
+
|
|
53
|
+
## Serialized payload
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
Whatsapp::MessageTemplates::Template.new(
|
|
57
|
+
name: "limited_time_offer_caribbean_pkg_2023", language: "en_US", category: "marketing",
|
|
58
|
+
components: [
|
|
59
|
+
{ type: :header, format: "image", header_handle: "4::aW" },
|
|
60
|
+
{ type: :limited_time_offer, text: "Expiring offer!", has_expiration: true },
|
|
61
|
+
{ type: :body, text: "Good news, {{1}}! Use code {{2}} to get 25% off!",
|
|
62
|
+
example: ["Pablo", "CARIBE25"] },
|
|
63
|
+
{ type: :buttons, buttons: [
|
|
64
|
+
{ type: :copy_code, example: "CARIBE25" },
|
|
65
|
+
{ type: :url, text: "Book now!", url: "https://x.test/offers?code={{1}}", example: "n3mtql" },
|
|
66
|
+
] },
|
|
67
|
+
]
|
|
68
|
+
).serialize
|
|
69
|
+
# => {
|
|
70
|
+
# name: "limited_time_offer_caribbean_pkg_2023", language: "en_US", category: "MARKETING",
|
|
71
|
+
# parameter_format: "POSITIONAL",
|
|
72
|
+
# components: [
|
|
73
|
+
# { type: "HEADER", format: "IMAGE", example: { header_handle: ["4::aW"] } },
|
|
74
|
+
# { type: "LIMITED_TIME_OFFER",
|
|
75
|
+
# limited_time_offer: { text: "Expiring offer!", has_expiration: true } },
|
|
76
|
+
# { type: "BODY", text: "Good news, {{1}}! Use code {{2}} to get 25% off!",
|
|
77
|
+
# example: { body_text: [["Pablo", "CARIBE25"]] } },
|
|
78
|
+
# { type: "BUTTONS", buttons: [
|
|
79
|
+
# { type: "COPY_CODE", example: "CARIBE25" },
|
|
80
|
+
# { type: "URL", text: "Book now!", url: "https://x.test/offers?code={{1}}",
|
|
81
|
+
# example: ["n3mtql"] }] },
|
|
82
|
+
# ]
|
|
83
|
+
# }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Media handles
|
|
87
|
+
|
|
88
|
+
`header_handle` comes from Meta's Resumable Upload API, not
|
|
89
|
+
[`Media#upload`](../media/README.md) — see
|
|
90
|
+
[README.md § Not wrapped](README.md#not-wrapped).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
**Meta docs:** <https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/limited-time-offer-templates>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Template Responses
|
|
2
|
+
|
|
3
|
+
Four endpoints, four response shapes — so four classes rather than one that is mostly
|
|
4
|
+
`nil`.
|
|
5
|
+
|
|
6
|
+
| Method | Returns |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `create`, `create_from_library`, `upsert` | [`Response::Created`](#created) |
|
|
9
|
+
| `find` | [`Response::Node`](#node) |
|
|
10
|
+
| `list` | [`Response::Collection`](#collection) |
|
|
11
|
+
| `update`, `delete` | plain `Boolean` |
|
|
12
|
+
|
|
13
|
+
## Created
|
|
14
|
+
|
|
15
|
+
`{id, status, category}` — what you get back from a submission.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
created = templates.create(name: "order_confirmation", language: "en_US",
|
|
19
|
+
category: "UTILITY", components: [...])
|
|
20
|
+
|
|
21
|
+
created.id # => "1259544702043867"
|
|
22
|
+
created.status # => "PENDING"
|
|
23
|
+
created.category # => "UTILITY"
|
|
24
|
+
|
|
25
|
+
created.pending? # => true
|
|
26
|
+
created.approved? # => false
|
|
27
|
+
created.rejected? # => false
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A [library template](library.md) usually comes back `APPROVED` immediately; everything
|
|
31
|
+
else starts `PENDING` and is reviewed asynchronously.
|
|
32
|
+
|
|
33
|
+
## Node
|
|
34
|
+
|
|
35
|
+
The full template object, from `find`.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
template = templates.find(template_id: "564750795574598")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Accessor | Example |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `id` | `"564750795574598"` |
|
|
44
|
+
| `name` | `"order_confirmation"` |
|
|
45
|
+
| `status` | `"APPROVED"` |
|
|
46
|
+
| `category` | `"UTILITY"` |
|
|
47
|
+
| `language` | `"en_US"` |
|
|
48
|
+
| `components` | **raw hashes** — see below |
|
|
49
|
+
| `parameter_format` | `"POSITIONAL"` |
|
|
50
|
+
| `sub_category` | `"ORDER_DETAILS"` |
|
|
51
|
+
| `rejected_reason` | `"NONE"` |
|
|
52
|
+
| `quality_score` | `#<QualityScore score="GREEN" date=1700000000 reasons=[]>` |
|
|
53
|
+
| `previous_category` | `"MARKETING"` |
|
|
54
|
+
| `correct_category` | `"UTILITY"` |
|
|
55
|
+
| `message_send_ttl_seconds` | `3600` |
|
|
56
|
+
| `cta_url_link_tracking_opted_out` | `false` |
|
|
57
|
+
| `library_template_name` | `"delivery_update_1"` |
|
|
58
|
+
|
|
59
|
+
Predicates: `#approved?`, `#rejected?`, `#paused?`, and `#editable?` (true for
|
|
60
|
+
`APPROVED`, `REJECTED`, and `PAUSED`).
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
template.status # => "APPROVED"
|
|
64
|
+
template.editable? # => true
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> **`#components` stays raw hashes on purpose.** It is not rebuilt into the
|
|
68
|
+
> `Component` classes. Those validate a payload being *written*; this is Meta's echo of
|
|
69
|
+
> one, carrying fields the write side does not model. Running write-side rules over
|
|
70
|
+
> data we did not author would raise on perfectly valid responses. Read it as data;
|
|
71
|
+
> build a new `Template` when you want to change something.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
template.components
|
|
75
|
+
# => [{ "type" => "BODY", "text" => "Thank you, {{1}}!",
|
|
76
|
+
# "example" => { "body_text" => [["Pablo"]] } }]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Collection
|
|
80
|
+
|
|
81
|
+
`{data, paging, summary}` from `list`. It is `Enumerable`, so it behaves like the array
|
|
82
|
+
of `Node`s it wraps.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
page = templates.list(status: %w[APPROVED], limit: 25)
|
|
86
|
+
|
|
87
|
+
page.map(&:name) # => ["order_confirmation", ...]
|
|
88
|
+
page.select(&:approved?) # Enumerable
|
|
89
|
+
page.next_cursor # => "AFTER" — nil on the last page
|
|
90
|
+
page.remaining # => 249
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Accessor | Holds |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `data` | `Array<Response::Node>` |
|
|
96
|
+
| `paging` | `Response::Paging` — `before`, `after` (flattened from `paging.cursors`) |
|
|
97
|
+
| `summary` | `Response::Summary` |
|
|
98
|
+
|
|
99
|
+
`#next_cursor` is `paging&.after`; `#remaining` is `summary&.remaining`.
|
|
100
|
+
|
|
101
|
+
### Summary
|
|
102
|
+
|
|
103
|
+
| Accessor | Meaning |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `total_count` | Templates matching the filter |
|
|
106
|
+
| `message_template_count` | Templates on the account |
|
|
107
|
+
| `message_template_limit` | The account's cap |
|
|
108
|
+
| `are_translations_complete` | Boolean |
|
|
109
|
+
| `remaining` | `limit - count`, or `nil` when either is missing |
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
page.summary.message_template_count # => 12
|
|
113
|
+
page.summary.message_template_limit # => 250
|
|
114
|
+
page.remaining # => 238
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Paging
|
|
118
|
+
|
|
119
|
+
Manual — there is no auto-pagination:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
page = templates.list(limit: 25)
|
|
123
|
+
all = page.to_a
|
|
124
|
+
|
|
125
|
+
while (cursor = page.next_cursor)
|
|
126
|
+
page = templates.list(limit: 25, after: cursor)
|
|
127
|
+
all.concat(page.to_a)
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### QualityScore
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
template.quality_score.score # => "GREEN" — GREEN | YELLOW | RED | UNKNOWN
|
|
135
|
+
template.quality_score.date # => 1_700_000_000
|
|
136
|
+
template.quality_score.reasons # => []
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Best-effort schema on `reasons` — Meta publishes no sub-fields for it.
|
|
140
|
+
|
|
141
|
+
## Booleans
|
|
142
|
+
|
|
143
|
+
`update` and `delete` return a plain `true`/`false` derived from `{"success": true}`:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
templates.update(template_id: "564750795574598", category: "MARKETING") # => true
|
|
147
|
+
templates.delete(name: "order_confirmation") # => true
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A non-2xx response raises `Whatsapp::MessageTemplates::TemplateError` instead of
|
|
151
|
+
returning `false`, so `false` genuinely means "Meta said no", not "the request failed".
|
|
152
|
+
|
|
153
|
+
## Statuses
|
|
154
|
+
|
|
155
|
+
| Status | Meaning |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `PENDING` | Under review |
|
|
158
|
+
| `APPROVED` | Sendable |
|
|
159
|
+
| `REJECTED` | Refused — see `rejected_reason` |
|
|
160
|
+
| `PAUSED` | Temporarily halted for quality |
|
|
161
|
+
| `DISABLED` | Permanently disabled; cannot be deleted |
|
|
162
|
+
| `IN_APPEAL` | Rejection under appeal |
|
|
163
|
+
| `PENDING_DELETION` | Deletion queued |
|
|
164
|
+
| `DELETED` | Gone |
|
|
165
|
+
| `LIMIT_EXCEEDED` | Account template cap hit |
|
|
166
|
+
| `ARCHIVED` | Archived |
|
|
167
|
+
|
|
168
|
+
`rejected_reason` is one of `NONE`, `ABUSIVE_CONTENT`, `INVALID_FORMAT`,
|
|
169
|
+
`PROMOTIONAL`, `TAG_CONTENT_MISMATCH`, `SCAM`.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
**Meta docs:** <https://developers.facebook.com/docs/graph-api/reference/whats-app-business-hsm/>
|