Skip to content

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, reaction or template) and include the object of the same name.
  • Recipient: to takes exactly one of:
    • contact_id: an existing contact's id.
    • wa_id: the phone number in international format, digits only, no leading + (for example 919876543210). If you don't hold that contact yet, it is created.
  • Sending number: phone_number_id picks which of your WhatsApp numbers sends. Leave it out if your organisation has one number.
  • Idempotency-Key header (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.

bash
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.

bash
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 send coupon_code.
  • components: WhatsApp's own header / body / button form with typed parameters. 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.

json
{
  "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: an https URL that IconEngage fetches once, when the message is accepted.
  • media_id: a file you uploaded earlier with POST /media. Upload once and reuse the id when the same file goes to many people.
bash
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

bash
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:

bash
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

bash
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

StatuscodeMeaning
400validation_failedA field breaks a WhatsApp rule (for example text too long, wrong media type, missing Idempotency-Key). details.fields names each field.
409window_expiredThe 24-hour window is closed. Send a template.
409template_requiredThe customer has never written to this number. Only a template can start the conversation.
409template_not_approvedThe template is not approved for this number, or the variables count is wrong.
409contact_opted_outThe customer has opted out. See Opt-outs and consent.
409conflict (details.reason: phone_number_not_connected)The sending number cannot send right now.
402subscription_restricted, usage_limit_reached, wallet_balance_lowYour 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.