curl --request POST \
--url https://api.ya.onl/v1/template-messages \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"event_id": "CART-8F3A21",
"phone": "+51987654321",
"template": {
"name": "cart_recovery",
"language": "es"
},
"variables": {
"1": "Juan",
"2": "S/ 1.250"
},
"occurred_at": "2026-08-11T13:22:05Z",
"metadata": {
"use_case": "cart_abandonment"
}
}
'import requests
url = "https://api.ya.onl/v1/template-messages"
payload = {
"event_id": "CART-8F3A21",
"phone": "+51987654321",
"template": {
"name": "cart_recovery",
"language": "es"
},
"variables": {
"1": "Juan",
"2": "S/ 1.250"
},
"occurred_at": "2026-08-11T13:22:05Z",
"metadata": { "use_case": "cart_abandonment" }
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
event_id: 'CART-8F3A21',
phone: '+51987654321',
template: {name: 'cart_recovery', language: 'es'},
variables: {'1': 'Juan', '2': 'S/ 1.250'},
occurred_at: '2026-08-11T13:22:05Z',
metadata: {use_case: 'cart_abandonment'}
})
};
fetch('https://api.ya.onl/v1/template-messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ya.onl/v1/template-messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'event_id' => 'CART-8F3A21',
'phone' => '+51987654321',
'template' => [
'name' => 'cart_recovery',
'language' => 'es'
],
'variables' => [
'1' => 'Juan',
'2' => 'S/ 1.250'
],
'occurred_at' => '2026-08-11T13:22:05Z',
'metadata' => [
'use_case' => 'cart_abandonment'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.ya.onl/v1/template-messages"
payload := strings.NewReader("{\n \"event_id\": \"CART-8F3A21\",\n \"phone\": \"+51987654321\",\n \"template\": {\n \"name\": \"cart_recovery\",\n \"language\": \"es\"\n },\n \"variables\": {\n \"1\": \"Juan\",\n \"2\": \"S/ 1.250\"\n },\n \"occurred_at\": \"2026-08-11T13:22:05Z\",\n \"metadata\": {\n \"use_case\": \"cart_abandonment\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.ya.onl/v1/template-messages")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"event_id\": \"CART-8F3A21\",\n \"phone\": \"+51987654321\",\n \"template\": {\n \"name\": \"cart_recovery\",\n \"language\": \"es\"\n },\n \"variables\": {\n \"1\": \"Juan\",\n \"2\": \"S/ 1.250\"\n },\n \"occurred_at\": \"2026-08-11T13:22:05Z\",\n \"metadata\": {\n \"use_case\": \"cart_abandonment\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ya.onl/v1/template-messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"event_id\": \"CART-8F3A21\",\n \"phone\": \"+51987654321\",\n \"template\": {\n \"name\": \"cart_recovery\",\n \"language\": \"es\"\n },\n \"variables\": {\n \"1\": \"Juan\",\n \"2\": \"S/ 1.250\"\n },\n \"occurred_at\": \"2026-08-11T13:22:05Z\",\n \"metadata\": {\n \"use_case\": \"cart_abandonment\"\n }\n}"
response = http.request(request)
puts response.read_body{
"event_id": "<string>",
"status": "accepted",
"status_detail": "stale"
}{
"event_id": "<string>",
"status": "accepted",
"status_detail": "stale"
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}Send a template message
Send an approved WhatsApp template to a customer.
A send that is taken answers 202 with status accepted as soon as the
sending service has it — the reply carries no delivery outcome. Poll
GET /template-messages/{event_id} for what became of it; no webhook
reports a send outcome today.
Idempotent on event_id: repeating one returns the state of the first
request and never produces a second message.
A 422 names the offending field: a phone outside strict E.164, a template
that is not approved for this merchant in that language, or a missing
template variable. It also covers the two account-level refusals that no
payload can fix — an account that is not on the conversational rail this
API sends from, and an account with no connected WhatsApp sender number —
each carrying its own type. Neither is worth a retry.
An event older than the freshness window, or one whose conversation is
paused, is not an error either: it answers 200 with status discarded
and the reason in status_detail.
A 402 means the account cannot pay for the message and nothing was sent.
insufficient_funds and wallet_sends_paused clear as soon as the account
adds funds, and the same event_id may be sent again once it has;
wallet_currency_mismatch needs us. Retrying without adding funds returns
the same answer.
curl --request POST \
--url https://api.ya.onl/v1/template-messages \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"event_id": "CART-8F3A21",
"phone": "+51987654321",
"template": {
"name": "cart_recovery",
"language": "es"
},
"variables": {
"1": "Juan",
"2": "S/ 1.250"
},
"occurred_at": "2026-08-11T13:22:05Z",
"metadata": {
"use_case": "cart_abandonment"
}
}
'import requests
url = "https://api.ya.onl/v1/template-messages"
payload = {
"event_id": "CART-8F3A21",
"phone": "+51987654321",
"template": {
"name": "cart_recovery",
"language": "es"
},
"variables": {
"1": "Juan",
"2": "S/ 1.250"
},
"occurred_at": "2026-08-11T13:22:05Z",
"metadata": { "use_case": "cart_abandonment" }
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
event_id: 'CART-8F3A21',
phone: '+51987654321',
template: {name: 'cart_recovery', language: 'es'},
variables: {'1': 'Juan', '2': 'S/ 1.250'},
occurred_at: '2026-08-11T13:22:05Z',
metadata: {use_case: 'cart_abandonment'}
})
};
fetch('https://api.ya.onl/v1/template-messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ya.onl/v1/template-messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'event_id' => 'CART-8F3A21',
'phone' => '+51987654321',
'template' => [
'name' => 'cart_recovery',
'language' => 'es'
],
'variables' => [
'1' => 'Juan',
'2' => 'S/ 1.250'
],
'occurred_at' => '2026-08-11T13:22:05Z',
'metadata' => [
'use_case' => 'cart_abandonment'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.ya.onl/v1/template-messages"
payload := strings.NewReader("{\n \"event_id\": \"CART-8F3A21\",\n \"phone\": \"+51987654321\",\n \"template\": {\n \"name\": \"cart_recovery\",\n \"language\": \"es\"\n },\n \"variables\": {\n \"1\": \"Juan\",\n \"2\": \"S/ 1.250\"\n },\n \"occurred_at\": \"2026-08-11T13:22:05Z\",\n \"metadata\": {\n \"use_case\": \"cart_abandonment\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.ya.onl/v1/template-messages")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"event_id\": \"CART-8F3A21\",\n \"phone\": \"+51987654321\",\n \"template\": {\n \"name\": \"cart_recovery\",\n \"language\": \"es\"\n },\n \"variables\": {\n \"1\": \"Juan\",\n \"2\": \"S/ 1.250\"\n },\n \"occurred_at\": \"2026-08-11T13:22:05Z\",\n \"metadata\": {\n \"use_case\": \"cart_abandonment\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ya.onl/v1/template-messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"event_id\": \"CART-8F3A21\",\n \"phone\": \"+51987654321\",\n \"template\": {\n \"name\": \"cart_recovery\",\n \"language\": \"es\"\n },\n \"variables\": {\n \"1\": \"Juan\",\n \"2\": \"S/ 1.250\"\n },\n \"occurred_at\": \"2026-08-11T13:22:05Z\",\n \"metadata\": {\n \"use_case\": \"cart_abandonment\"\n }\n}"
response = http.request(request)
puts response.read_body{
"event_id": "<string>",
"status": "accepted",
"status_detail": "stale"
}{
"event_id": "<string>",
"status": "accepted",
"status_detail": "stale"
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"type": "<string>",
"detail": "<string>"
}{
"type": "<string>",
"detail": "<string>"
}Autorizaciones
Merchant API key for authentication
Cuerpo
Public request body for a template send.
Only event_id is shape-checked here. Everything else is validated by the
service in the order the contract declares, so a replay of an already-seen
event_id short-circuits before any other field is judged.
Your identifier for the event. Idempotency key and lookup key for the status endpoint; must be stable across retries. The charset is bounded because this value travels in the status URL
^[A-Za-z0-9._-]{8,128}$"CART-8F3A21"
Destination phone in strict E.164 (+ country code, 8-15 digits). A number outside the format is rejected with 422 — the country code is never inferred
"+51987654321"
Approved template to send, by name and language
Show child attributes
Show child attributes
Template variable values, keyed exactly as required_variables lists them in GET /v1/templates: by position on a positional template ("1" fills {{1}}), by name on a template authored with named placeholders ("name" fills {{name}}). A missing or unknown key is a 422
Show child attributes
Show child attributes
{ "1": "Juan", "2": "S/ 1.250" }
{ "name": "Juan" }
ISO 8601 UTC timestamp of the originating event. An event older than the freshness window is discarded instead of sent
"2026-08-11T13:22:05Z"
Optional attribution metadata
Show child attributes
Show child attributes
Respuesta
No new send was produced: the event_id had already been received and the stored state is returned, or the event settled without being sent
The event_id sent in the request
Current status of the send. A send that was taken answers accepted; poll the status endpoint for what became of it. sent, delivered, read and failed come from delivery receipts, and discarded means it was never sent
accepted, in_progress, sent, delivered, read, failed, discarded Reason behind a failed or discarded status, or the delivery_not_observable note on an accepted one. Not a closed set, and may be absent. stale, paused, account_not_eligible, no_sender_number, send_failed, send_cancelled and delivery_not_observable are ours and stable — the last one means the send went to a store this deployment cannot read, so the status will not settle here; when the provider refused the delivery it carries provider_error_<code> with the provider's own numeric code (for example provider_error_131049), and that catalogue changes without notice. Branch on status, log this, and treat an unrecognised or missing value as a generic failure
"stale"

