Templates and buttons
Templates are the notifications you can send with
POST /messages: texts per language with variables, up to 3
buttons and a review by xYago before they can be used in production. You create them in the
panel (API Developer → Templates); the API only reads them
(GET /templates).
Text
- Service notifications only (category
utility): never advertising. - One text per language (
es,en,de), 1 to 1,024 characters, with numbered variables{{1}},{{2}}... (the same in every language, max. 20). - No links in the text: a text with
http://,https://orwww.is rejected with422 link_in_body_not_allowed. Use aurlbutton to link.
Buttons
A template can have up to 3 buttons, shown below the message in the same
order. Like the body, the button text is per language (texts,
with the same shape as languages: e.g.
{"es": "Seguir pedido", "en": "Track order"}): one for each language of the
template, max. 20 characters, no variables and different from the other buttons. Each message
shows the buttons in the same language as the body.
| Type | Fields | What happens when tapped |
|---|---|---|
quick_reply | texts, payload (optional, max. 128, fixed in the template) | You receive the message.button_clicked webhook with your payload. It is not a message: the user types nothing and the payload never reaches their phone. |
url (max. 2) | texts, url | The link opens (and you receive message.button_clicked with the final URL). |
url rules: https:// only, with a domain name (not an IP), no user
or password and no URL shorteners (bit.ly, tinyurl.com, t.co…). It can contain
one {{1}} variable in the path or the query
(never in the domain), e.g.
https://tiendaexpress.example.com/pedidos/{{1}}. Its value is
sent with each message in button_variables, by button index:
{"to": "+573005550123", "sender_id": "tiendaexpress", "template": "pedido_enviado",
"variables": ["Valentina", "TE-48213"], "button_variables": {"0": "TE-48213"}}
The value is text (max. 200 characters) and xYago URL-encodes it. A missing value for a
button with a variable, an unexpected one or an invalid one →
422 invalid_button_variables.
Example template (GET /templates/{id}):
{
"id": "d2f4a6c8-0e1b-4d3f-9a5c-7e9b1d3f5a70",
"sender_id": "tiendaexpress",
"name": "pedido_enviado",
"category": "utility",
"default_language": "es",
"languages": {
"es": "Hola {{1}}, tu pedido {{2}} va en camino.",
"en": "Hi {{1}}, your order {{2}} is on its way."
},
"variables_count": 2,
"buttons": [
{
"type": "url",
"texts": {
"es": "Seguir pedido",
"en": "Track order"
},
"payload": null,
"url": "https://tiendaexpress.example.com/pedidos/{{1}}"
},
{
"type": "quick_reply",
"texts": {
"es": "No lo pedí",
"en": "Not my order"
},
"payload": "order_not_mine",
"url": null
}
],
"version": 1,
"status": "approved",
"review_flags": [],
"rejection_reason": null,
"created_at": "2026-11-03T10:00:00Z",
"updated_at": "2026-11-03T16:45:00Z"
}
Review by xYago
- You create the template (
draft) and submit it for review (pending_review). In sandbox you can already test adraft. - On submission automatic checks (
review_flags) are computed; the reviewer and you see them in the panel. They do not block the submission, but you should fix them:
| Flag | Meaning |
|---|---|
marketing_words | Advertising words (discount, offer, free, coupon…). Templates are for service notifications only. |
url_shortener | Mentions a URL shortener. |
too_many_variables | More than 10 variables. |
variables_adjacent | Two variables in a row without text between them. |
starts_or_ends_with_variable | The text starts or ends with a variable. |
only_variables | The text is only variables. |
xYago approves it (approved) or rejects it with a reason
(rejected; fix it in the panel and it goes back to review). Every step is kept in
its history in the panel and emits template.status_changed.
Versions
An approved template does not change. Editing it creates the next
version (same name, version + 1) in review, and the approved
one keeps being used for your sends until xYago approves the new one; then the previous one
becomes superseded. Nothing changes in your integration: you send by
name.
If xYago pauses a template (e.g. after user complaints), sends with it
return 409 template_paused until it is reactivated.
Statuses
| Status | Meaning |
|---|---|
draft | Draft (sandbox only). |
pending_review | Being reviewed by xYago. |
approved | Usable in live and sandbox. |
rejected | Rejected (see rejection_reason). |
paused | Paused by xYago: 409 template_paused. |
superseded | Replaced by a newer approved version. |
Click webhook
Subscribe to message.button_clicked. Only the first tap of
each button of each message is reported. data contains the message (as in
message.*, with your reference), the button and the click time. The
button text is the one in the language the message was sent in
(button.language, same as message.language):
{
"created_at": "2026-03-02T15:00:04Z",
"data": {
"message": {
"id": "7a1c9e35-2b4d-4f6a-8c0e-1d3f5a7b9c21",
"status": "read",
"to_masked": "+57 ••• ••• 0123",
"sender_id": "tiendaexpress",
"template": "pedido_enviado",
"language": "es",
"reference": "CLI-000123",
"failure_code": null,
"created_at": "2026-11-04T15:00:00Z",
"sent_at": "2026-11-04T15:00:01Z",
"delivered_at": "2026-11-04T15:02:10Z",
"read_at": "2026-11-04T15:07:42Z",
"expires_at": null
},
"button": {
"index": 1,
"id": "b1",
"type": "quick_reply",
"text": "No lo pedí",
"language": "es",
"payload": "order_not_mine",
"url": null
},
"clicked_at": "2026-11-04T15:08:03Z"
},
"environment": "live",
"id": "evt_4f9c2b7a1d3e4c5b8a6f0e1d2c3b4a59",
"type": "message.button_clicked"
}