Skip to content

Send a template message

Template messages are pre-approved by WhatsApp. They are the only message type you can send to someone outside the 24-hour window, so they’re how you start conversations: order updates, OTPs, reminders, campaigns.

The template must exist on the sending number’s WABA and be APPROVED. Use GET /v1/{waba_id}/templates to look up its exact name and language, or let the send fill in the language automatically when the name has only one.

A template with no variables:

Terminal window
curl -X POST "$HELLO_API_URL/v1/<your-phone-number-id>/messages" \
-H "Authorization: Bearer $HELLO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+919999999999",
"type": "template",
"template": {
"name": "hello_world",
"language": { "code": "en_US" }
}
}'

Response (202 Accepted):

{
"messaging_product": "whatsapp",
"contacts": [{ "input": "+919999999999", "wa_id": "919999999999" }],
"messages": [{ "id": "b7e1d2c3-4f5a-4b6c-8d9e-0a1b2c3d4e5f", "message_status": "queued" }]
}

Add ?sync=true to wait up to 10 seconds for WhatsApp to accept it. See Queued and synchronous sends.

template.language.code can be left out when the name exists in only one language on the number’s WABA — Hello looks it up and fills in the stored spelling. If the name exists in several languages, leaving it out returns 400 TEMPLATE_LANGUAGE_REQUIRED.

For a template body such as Hi {{1}}, your order {{2}} has shipped., pass one text parameter per variable, in order:

{
"to": "+919999999999",
"type": "template",
"template": {
"name": "order_shipped",
"language": { "code": "en" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Asha" },
{ "type": "text", "text": "#10432" }
]
}
]
}
}

For a template created with named parameters, for example Hi {{first_name}}, your OTP is {{otp}}., add parameter_name to each parameter. Order doesn’t matter:

{
"to": "+919999999999",
"type": "template",
"template": {
"name": "login_otp",
"language": { "code": "en" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "first_name", "text": "Asha" },
{ "type": "text", "parameter_name": "otp", "text": "482913" }
]
}
]
}
}

For a template with an IMAGE, VIDEO or DOCUMENT header, pass the media as a header parameter. Use either a link to a publicly reachable HTTPS URL, or an id from POST /v1/media/upload:

{
"to": "+919999999999",
"type": "template",
"template": {
"name": "invoice_ready",
"language": { "code": "en" },
"components": [
{
"type": "header",
"parameters": [
{
"type": "document",
"document": {
"link": "https://example.com/invoices/INV-2291.pdf",
"filename": "INV-2291.pdf"
}
}
]
},
{
"type": "body",
"parameters": [{ "type": "text", "text": "INV-2291" }]
}
]
}
}

The same shape works for images ("type": "image", "image": { "id": "1234567890" }) and videos ("type": "video", "video": { "link": "https://…/clip.mp4" }). filename only applies to documents. It is the file name the recipient sees.

A TEXT header with a variable takes a text parameter:

{ "type": "header", "parameters": [{ "type": "text", "text": "Asha" }] }

A LOCATION header takes a location parameter. Here latitude and longitude are strings:

{
"type": "header",
"parameters": [
{
"type": "location",
"location": {
"latitude": "12.9716",
"longitude": "77.5946",
"name": "Indiranagar store",
"address": "100 Feet Rd, Bengaluru"
}
}
]
}

Body parameters can also be currency or date_time:

{
"type": "body",
"parameters": [
{
"type": "currency",
"currency": { "fallback_value": "₹1,499.00", "code": "INR", "amount_1000": 1499000 }
},
{
"type": "date_time",
"date_time": { "fallback_value": "26 September 2026" }
}
]
}

amount_1000 is the amount multiplied by 1000.

A button component supplies the value a dynamic URL suffix or copy-code button needs at send time — see Buttons for the full shape. It’s forwarded to WhatsApp along with header and body.

Field Type Required Description
template.name string Yes Template name exactly as approved (matched case-insensitively)
template.language.code string If the name has more than one language Language code of the approved translation, for example en, en_US, hi
template.components array If the template has variables or dynamic buttons One entry per component that has variables
components[].type string Yes header, body or button
components[].parameters[] array Yes Values in the order the variables appear, or named with parameter_name
parameters[].type string Yes text, image, video, document, location, currency or date_time

See Send a message for the fields shared by every message type.

For complete walkthroughs of the richer layouts, see header & footer, buttons and media with buttons.

Status code Cause
400 VALIDATION_ERROR to, type or template.name is missing
400 TEMPLATE_LANGUAGE_REQUIRED The name exists in several languages and none was given — available_languages lists them
403 TEMPLATE_NOT_APPROVED The template exists but isn’t APPROVED
403 PLAN_CATEGORY_NOT_ALLOWED Your plan doesn’t allow this template’s category
404 TEMPLATE_NOT_FOUND No template with this name on the number’s WABA
404 TEMPLATE_LANGUAGE_NOT_FOUND The name exists but not in the requested language
422 SEND_FAILED (?sync=true only) WhatsApp rejected the template, for example a wrong parameter count

When a queued template send fails later, the message’s status becomes failed and its error_code and error_message explain why. Check with GET /v1/messages/{id}.