POST /v1/actions/send-template-message
Send a Meta-approved WhatsApp template message to a contact. Works any time — including outside the 24h customer service window.
Sends a Meta-approved template message. Templates are the only way to message a contact outside the 24-hour customer service window — and the only way to start a conversation with someone you've never messaged.
The contact is upserted by (tenant, phone) automatically — you don't need to call create-contact first.
POST /api/v1/actions/send-template-message
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
phone |
string | yes | International format with + (e.g. +14155550123). Qyvo normalizes minor variations. |
template_id |
UUID | yes | The Qyvo template id (not the Meta name). List templates to discover ids. |
language |
string | no | e.g. en, fr. If omitted (or not approved), Qyvo falls back to any approved translation. |
variables |
object | conditional | Required when the template body has {{1}}, {{2}} placeholders. Keys are the placeholder names, values are strings. |
curl -X POST https://www.qyvo.io/api/v1/actions/send-template-message \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"phone": "+14155550123",
"template_id": "01J0AB...",
"language": "en",
"variables": {
"1": "Romain",
"2": "ORD-1042"
}
}'
const res = await fetch('https://www.qyvo.io/api/v1/actions/send-template-message', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.QYVO_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone: '+14155550123',
template_id: '01J0AB...',
language: 'en',
variables: { 1: 'Romain', 2: 'ORD-1042' },
}),
});
Http::withToken(env('QYVO_TOKEN'))
->post('https://www.qyvo.io/api/v1/actions/send-template-message', [
'phone' => '+14155550123',
'template_id' => '01J0AB...',
'language' => 'en',
'variables' => ['1' => 'Romain', '2' => 'ORD-1042'],
])
->json();
import os, httpx
httpx.post(
'https://www.qyvo.io/api/v1/actions/send-template-message',
headers={'Authorization': f"Bearer {os.environ['QYVO_TOKEN']}"},
json={
'phone': '+14155550123',
'template_id': '01J0AB...',
'language': 'en',
'variables': {'1': 'Romain', '2': 'ORD-1042'},
},
).raise_for_status()
Response — 200 OK
{
"id": "01J1Z...",
"status": "sent",
"whatsapp_message_id": "wamid.HBgL...",
"contact_id": "01J1Y..."
}
| Field | Notes |
|---|---|
id |
Qyvo message id — appears in the Inbox; receives subsequent delivered/read status updates |
status |
sent — Meta accepted the request. Async statuses (delivered, read, failed) flow through webhooks. |
whatsapp_message_id |
Meta's wamid.* — useful for cross-referencing with Meta's logs |
contact_id |
The contact (created if it didn't exist) |
Errors
| Status | Body | Cause |
|---|---|---|
404 |
Template not found |
The template_id doesn't match any template in your tenant |
422 |
Template requires variables that were not provided. |
Body has {{N}} you didn't pass. Response includes expected_placeholders and missing. |
422 |
Template has no approved translation to send. |
Template is PENDING/REJECTED on Meta. Response includes available_languages. |
422 |
No WhatsApp account configured |
Your tenant has no WabaAccount — onboard a number first |
422 |
Meta error message bubbled up | See Errors → Meta codes |
Variables and placeholders
The template body might be:
Hi {{name}}, your order {{order}} is shipping today. Track it: {{tracking_url}}
Placeholders are extracted by Qyvo from the body. The variables keys must match — both numeric (1, 2, …) and named placeholders are supported, depending on how you authored the template on Meta.
If you pass too few keys, you'll get back:
{
"error": "Template requires variables that were not provided.",
"expected_placeholders": ["name", "order", "tracking_url"],
"missing": ["tracking_url"]
}
Values must be strings — coerce numbers/dates client-side.
