Send a message with the API
Send a text or template WhatsApp message, upload media, avoid duplicate sends with an idempotency key, and check where a message has got to.
POST /messages sends any WhatsApp message type from one of your connected numbers. This article shows a text message, a template message, an image, and how to check a message's status afterwards.
The key needs the messages.send permission. To address a customer by phone number (rather than an existing contact id), it also needs contacts.create.
The basics
- Body shape: WhatsApp Cloud API's own message shapes. Set
type(text,image,video,audio,document,sticker,location,contacts,interactive,reactionortemplate) and include the object of the same name. - Recipient:
totakes exactly one of:contact_id: an existing contact's id.wa_id: the phone number in international format, digits only, no leading+(for example919876543210). If you don't hold that contact yet, it is created.
- Sending number:
phone_number_idpicks which of your WhatsApp numbers sends. Leave it out if your organisation has one number. Idempotency-Keyheader (required): a unique value per message you intend to send, 8 to 128 characters of letters, digits,.,_,:or-, starting with a letter or digit. If a request times out, retry with the same key: you get the original message back and the customer does not receive it twice.
Send a text message
Text and other free-form messages are only accepted while the customer's 24-hour window is open: within 24 hours of their last message to you. See The 24-hour window and templates.
curl -X POST https://iconengage.com/api/public/v1/messages \
-H "Authorization: Bearer $ICONENGAGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-shipped" \
-d '{
"to": { "wa_id": "919876543210" },
"type": "text",
"text": { "body": "Your order #1042 has shipped." }
}'The body can be up to 4,096 characters.
Send a template message
Use an approved template to message a customer outside the 24-hour window, or one who has never written to you. Name the template and its language, and fill its variables.
curl -X POST https://iconengage.com/api/public/v1/messages \
-H "Authorization: Bearer $ICONENGAGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-confirmation" \
-d '{
"to": { "wa_id": "919876543210" },
"type": "template",
"template": {
"name": "order_confirmation",
"language": "en",
"variables": ["Asha", "#1042"]
}
}'You can fill the template in one of two ways, never both:
variables: a simple list with one value per numbered variable ({{1}},{{2}}, …) in order. A value fills every place that number appears. If the template has a copy-code button, also sendcoupon_code.components: WhatsApp's ownheader/body/buttonform with typedparameters. Use it for currency or date values, your own quick-reply payloads, or a media header.
The values are checked against the approved template before the message is accepted. Each value must be non-empty and must not contain a new line, a tab or more than four spaces in a row.
See Template variables and buttons for how variables are set up.
The response
A new message is answered 202 Accepted. This means IconEngage has stored the message and will send it. It has not reached WhatsApp yet.
{
"id": "6f1c2e8a-…",
"conversation_id": "b0e4…",
"contact_id": "2a7d…",
"direction": "outbound",
"type": "template",
"status": "accepted",
"template_name": "order_confirmation",
"text": null,
"media_id": null,
"wamid": null,
"created_at": "2026-10-11T09:30:00Z",
"sent_at": null,
"delivered_at": null,
"read_at": null,
"failed_at": null,
"failure_code": null,
"failure": null
}Repeating a request with the same Idempotency-Key and the same body returns 200 with the original message in its current state, and sends nothing. The same key with a different body is refused 409 duplicate_request.
Send an image or other media
Media messages (image, video, audio, document, sticker) take exactly one of:
link: anhttpsURL that IconEngage fetches once, when the message is accepted.media_id: a file you uploaded earlier withPOST /media. Upload once and reuse the id when the same file goes to many people.
curl -X POST https://iconengage.com/api/public/v1/messages \
-H "Authorization: Bearer $ICONENGAGE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-invoice" \
-d '{
"to": { "wa_id": "919876543210" },
"type": "document",
"document": { "media_id": "c3d9…", "filename": "Invoice-1042.pdf", "caption": "Your invoice" }
}'A caption (up to 1,024 characters) is allowed on an image, a video or a document. A filename is allowed on a document only.
Upload a file
curl -X POST https://iconengage.com/api/public/v1/media \
-H "Authorization: Bearer $ICONENGAGE_API_KEY" \
-F "[email protected];type=application/pdf"Or import from a URL:
curl -X POST https://iconengage.com/api/public/v1/media \
-H "Authorization: Bearer $ICONENGAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/files/Invoice-1042.pdf" }'The answer is 201 with the file's media_id. Rules:
- The file's declared type must match its contents, and WhatsApp must support the type.
- Files can be up to 16 MiB. WhatsApp's per-type limits are checked when you send, for example an image is at most 5 MB.
- A URL must be public
https. IconEngage follows at most 3 redirects and gives up after 30 seconds. - An upload counts as 5 requests against your quota.
- An organisation can run 2 URL imports at once. A third gets
429 rate_limited; wait and retry.
Check a message's status
curl https://iconengage.com/api/public/v1/messages/6f1c2e8a-… \
-H "Authorization: Bearer $ICONENGAGE_API_KEY"status moves from accepted to sending, sent (WhatsApp has it), delivered and read, or ends at failed. WhatsApp can fail a message even after sent. When it fails, failure explains why: our code, WhatsApp's provider_code (for example 131042 for a payment problem on the WhatsApp Business Account), WhatsApp's own wording, a plain explanation, and provider_href when WhatsApp offers a link to fix the cause.
Rather than polling, subscribe to the API: message status event. See Event webhooks.
Common refusals
| Status | code | Meaning |
|---|---|---|
400 | validation_failed | A field breaks a WhatsApp rule (for example text too long, wrong media type, missing Idempotency-Key). details.fields names each field. |
409 | window_expired | The 24-hour window is closed. Send a template. |
409 | template_required | The customer has never written to this number. Only a template can start the conversation. |
409 | template_not_approved | The template is not approved for this number, or the variables count is wrong. |
409 | contact_opted_out | The customer has opted out. See Opt-outs and consent. |
409 | conflict (details.reason: phone_number_not_connected) | The sending number cannot send right now. |
402 | subscription_restricted, usage_limit_reached, wallet_balance_low | Your plan, allowance or WhatsApp credit balance stops the send. See WhatsApp credits. |
For every status code, see Rate limits and errors. If messages are accepted but never arrive, see Messages not delivered.
Still need help?
Write to us at [email protected] and tell us what you were trying to do. Already a customer? Sign in to IconEngage — the same articles are under Help in the app.