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.
Minimal example
Section titled “Minimal example”A template with no variables:
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.
Body variables (positional)
Section titled “Body variables (positional)”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" } ] } ] }}Body variables (named)
Section titled “Body variables (named)”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" } ] } ] }}Media header
Section titled “Media header”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.
Text and location headers
Section titled “Text and location headers”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" } } ]}Currency and date-time parameters
Section titled “Currency and date-time parameters”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.
Buttons
Section titled “Buttons”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.
Fields
Section titled “Fields”| 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.
Common errors
Section titled “Common errors”| 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}.