Skip to content

Send a template message

This guide sends one WhatsApp template message from your server and follows it until it is read.

A template is a message that Meta approved in advance. You need a template to start a conversation, or to write to someone who has not written to you in the last 24 hours.

You need an API key with the send scope. See Authentication. The examples read the key from the environment variable SANADWA_API_KEY.

The phone_number_id is the id of the WhatsApp number you send from. Get it once and store it in your configuration. In the console it is called the Sender ID (Settings → Developers → Senders).

Terminal window
curl https://api.sanadwa.com/v1/phone-numbers \
-H "Authorization: Bearer $SANADWA_API_KEY"
{
"success": true,
"data": [
{
"phone_number_id": "100000000000001",
"display_phone_number": "+1 202-555-0100",
"verified_name": "Example Store",
"name_status": "APPROVED",
"new_name_status": null,
"waba_id": "200000000000002",
"quality_rating": "GREEN",
"status": "CONNECTED"
}
]
}

The list has every active number in your account. The values of name_status, quality_rating and status come from WhatsApp (Meta). waba_id is the id of the WhatsApp Business Account that the number belongs to. Templates belong to a WhatsApp Business Account, so a number can only send the templates of its own account.

If the number is not ready to send yet (the console’s Senders panel shows Not ready to send), the API accepts your request but WhatsApp refuses the message with send_failed.

List the templates that this number can send. Pass the phone_number_id from step 1:

Terminal window
curl "https://api.sanadwa.com/v1/templates?phone_number_id=100000000000001&limit=200" \
-H "Authorization: Bearer $SANADWA_API_KEY"
{
"success": true,
"page": 1,
"limit": 200,
"data": [
{
"name": "order_update",
"language": "en",
"category": "UTILITY",
"status": "APPROVED",
"waba_id": "200000000000002",
"components": [
{ "type": "BODY", "text": "Hello {{1}}, your order {{2}} has shipped." }
]
}
]
}

The list has only templates that Meta has approved. You can also search by name or language with q, for example ?q=order. If you have many templates, read all pages; see Pagination.

From the template, note:

  • name and language: you send both.
  • The variables in the text: {{1}}, {{2}}, and so on. You send one value for each.
  • category: MARKETING, UTILITY or AUTHENTICATION. It affects the price and the rules. For example, people who opted out of marketing cannot receive MARKETING templates.

Send POST /messages/template with:

Field Required Meaning
phone_number_id Yes The number from step 1.
to_phone Yes The recipient, in E.164 format, for example +12025550123.
template_name Yes The template’s name.
template_language No, but send it The template’s language. If you leave it out, the API uses ar.
template_params If the body has variables A list of strings, one for each body variable, in order: the first value fills {{1}}.
template_components No Advanced: header or button variables. See Header and button variables.

Always send an Idempotency-Key header, so that a retry cannot send the message twice. See Idempotency.

Terminal window
curl https://api.sanadwa.com/v1/messages/template \
-H "Authorization: Bearer $SANADWA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a8e-3b7d-4c55-9a1e-2d4f6b8c0e12" \
-d '{
"phone_number_id": "100000000000001",
"to_phone": "+12025550123",
"template_name": "order_update",
"template_language": "en",
"template_params": ["Sara", "A-1042"]
}'

A 200 response means WhatsApp accepted the message. It does not yet mean the message was delivered.

{
"success": true,
"message_id": "wamid.HBgLMTIwMjU1NTAxMjMVAgARGBI",
"accepted_at": "2026-11-11T08:00:01.000Z",
"upstream": {
"messaging_product": "whatsapp",
"contacts": [{ "input": "+12025550123", "wa_id": "12025550123" }],
"messages": [{ "id": "wamid.HBgLMTIwMjU1NTAxMjMVAgARGBI" }]
}
}

Store message_id with your own record. It is the WhatsApp message id; status updates refer to it. upstream is the response that SanadWA received from WhatsApp.

Common errors at this step:

Code Why What to do
validation_error (400) A required field is missing or has the wrong type. Read message; it names the field.
phone_not_found (404) The phone_number_id is not an active number in your account. Repeat step 1.
PREPAID_CREDIT_CAP_REACHED (402) Not enough credits. Buy credits in the console.
MARKETING_SUPPRESSED (409) The recipient does not receive marketing from you. Do not send marketing to this person.
send_failed (varies) WhatsApp refused the message, for example because the template name, language or number of variables is wrong. Read message and meta_error.

The full list is on the Errors page.

template_params fills only the variables in the template’s body. If your template has variables in the header or in buttons, send template_components instead. It is passed to WhatsApp unchanged, in the format of the WhatsApp Cloud API. When you send template_components, template_params is ignored, so include the body variables in template_components too.

{
"phone_number_id": "100000000000001",
"to_phone": "+12025550123",
"template_name": "order_shipped_with_header",
"template_language": "en",
"template_components": [
{ "type": "header", "parameters": [{ "type": "text", "text": "A-1042" }] },
{ "type": "body", "parameters": [{ "type": "text", "text": "Sara" }] }
]
}

For an authentication template (a one-time code with a copy-code button), send the code as the first value of template_params. SanadWA also puts it into the button for you.

After WhatsApp accepts the message, it reports what happens to it: delivered, read, or failed. SanadWA records each report. You can ask for the current status, or be told when it changes.

Call GET /messages/{id} with the message_id from step 3:

Terminal window
curl https://api.sanadwa.com/v1/messages/wamid.HBgLMTIwMjU1NTAxMjMVAgARGBI \
-H "Authorization: Bearer $SANADWA_API_KEY"
{
"success": true,
"data": {
"id": 98765,
"provider_msg_id": "wamid.HBgLMTIwMjU1NTAxMjMVAgARGBI",
"status": "delivered",
"sent_at": "2026-11-11T08:00:01.000Z",
"template_name": "order_update",
"to_phone": "+12025550123",
"channel": "whatsapp",
"provider": "waba",
"error": null,
"delivered_at": "2026-11-11T08:00:04.000Z",
"read_at": null,
"failed_at": null,
"last_status": "delivered"
}
}

status of a message sent with this API:

status Meaning
sent WhatsApp accepted the message. This is the status right after the send call.
delivered WhatsApp reported that the message reached the recipient’s phone. delivered_at is set.
read WhatsApp reported that the recipient opened it. read_at is set.
failed WhatsApp reported that it could not deliver the message. error holds WhatsApp’s reason.

Good to know:

  • The status changes only when WhatsApp’s report reaches SanadWA. Reports can come late, for example when the recipient’s phone is switched off. Do not expect a fixed delay.
  • The status only moves forward: sent, then delivered, then read. A late or repeated report never moves it back, and a message that was delivered never becomes failed.
  • For messages sent with this API, failed_at is always null. A failure shows as status failed with error. last_status is the same as status.
  • You can also look up messages sent from the console inbox and from campaigns. Messages that customers sent to you are not returned.
  • Any id that does not start with wamid., or that is not in your account, answers 404 message_not_found.
  • Every lookup counts toward your rate limit. Do not check the same message every second; check now and then, or use webhooks.

Instead of asking again and again, set up a webhook. Subscribe to these events:

Event Meaning
message.sent WhatsApp accepted the message (sent right after a successful API call).
message.delivered The message reached the recipient’s phone.
message.read The recipient opened it.
message.failed It could not be delivered. data.error says why.

In the status events, match data.provider_msg_id to the message_id you stored in step 3:

// inside your webhook handler, after checking the signature
if (["message.delivered", "message.read", "message.failed"].includes(event.event)) {
const order = await findOrderByWhatsappId(event.data.provider_msg_id);
await order.updateWhatsappStatus(event.event, event.data);
}

When the customer answers, you receive an inbound.message webhook. For the next 24 hours you may reply with free-form text using POST /messages/text, without a template:

Terminal window
curl https://api.sanadwa.com/v1/messages/text \
-H "Authorization: Bearer $SANADWA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"phone_number_id": "100000000000001",
"to_phone": "+12025550123",
"text": "Thank you, Sara. Your order arrives tomorrow."
}'

After 24 hours without a message from the customer, this returns 409 customer_service_window_closed, and you must send a template again.