Sending

Send a message

An approved template to one person, or a file to somebody already talking to you. The call returns as soon as we have accepted it, and the delivery happens behind it.

The request

shell
curl -X POST https://sendrixbackend.exebee.com/v1/messages \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4471-shipped" \
  -d '{
    "to": "919833663235",
    "template": {
      "name": "order_shipped",
      "language": "en",
      "variables": { "1": "Asha", "2": "BK-4471" }
    }
  }'
json
HTTP/1.1 202 Accepted

{
  "id": "msg_c16cead6-8385-481f-9e2e-e146d51f2738",
  "object": "message",
  "to": "919833663235",
  "status": "queued",
  "template_id": "tpl_cb9182c4-6e22-4095-bd26-a61c9f8d7fba",
  "wa_message_id": null,
  "error": null,
  "created_at": "2026-08-28T15:03:25.424Z",
  "sent_at": null, "delivered_at": null, "read_at": null, "failed_at": null
}

Requires messages:send. A 202means we have taken it and accounted for it — one credit, or one message off your plan’s allowance. It does not mean WhatsApp has it yet.

Sending a file

There are two ways, and which one you need depends entirely on whether the person has messaged you recently.

On a template, to anybody

A template whose header is an image, video or document carries the file with it, and a template reaches anybody. This is what an order confirmation with the product photo, or a shipping note with the label attached, actually is.

shell
curl -X POST https://sendrixbackend.exebee.com/v1/messages \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4471-invoice" \
  -d '{
    "to": "919833663235",
    "template": {
      "name": "order_invoice",
      "variables": { "1": "Asha", "2": "BK-4471" },
      "header_media": {
        "link": "https://yourshop.example/invoices/4471.pdf",
        "filename": "invoice-4471.pdf"
      }
    }
  }'

requires_media_header on the template listing tells you which of your templates need this. Send one without it and you get 400 header_media_required rather than a message that fails at WhatsApp after we have already accepted it.

On its own, inside the 24 hour window

A bare file, with no template around it. It only reaches somebody who has messaged you in the last 24 hours — that is WhatsApp’s rule, not ours — which makes it a reply, not a way to start a conversation.

shell
curl -X POST https://sendrixbackend.exebee.com/v1/messages \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "919833663235",
    "media": {
      "type": "image",
      "link": "https://yourshop.example/receipts/4471.jpg",
      "caption": "Your receipt"
    }
  }'
FieldRequiredNotes
media.typeyesimage, video, audio or document.
media.linkone of these twoA public URL. WhatsApp fetches it at send time, so it has to be reachable from their network — not behind a login, and not a signed URL that expires in seconds.
media.idone of these twoAn id from POST /v1/media. Use this when the file is not public, or when the same file goes to many people.
media.captionnoA line under the file. Not available on audio, which WhatsApp gives no caption.
media.filenamenoDocuments only. What the file is called when it arrives.

A bare file costs nothing. WhatsApp stopped charging for service messages in July 2025, so this is free, exactly like the same attachment sent from your inbox. A template always costs a credit, media header or not.

Why it does not wait

Sending inline would mean your checkout holding a connection open across a call to Meta that can take thirty seconds, with nothing to retry it when that fails. Queuing means your message gets the same retries, pacing and rate-limit backoff that campaigns get, and your request comes back in milliseconds.

In practice it leaves for Meta well under a second later. You learn the outcome from webhooks, or by reading the message back.

Fields

FieldRequiredNotes
toyesThe recipient in international form. Spaces, dashes and a leading + are all fine; we normalise it. An impossible number is rejected before it costs anything.
template.nameyesMust be approved and sendable. See Templates.
template.languagenoNeeded only when the same template name exists in several languages.
template.variablesif the template has anyExactly the keys in that template’s required_keys. No more, no fewer.
template.header_mediaif the template has a media headerThe image, video or document that fills the header. { "link": "https://…" } for a public URL we hand to WhatsApp, or { "id": "…" } for something from POST /v1/media. Exactly one. Add filename on a document.
sender_idnoWhich number to send from. Omit it and your default is used.
contact_namenoSaved against the contact if we have not seen this number before. Shows up in your inbox.

Retrying safely

Send an Idempotency-Key header on every send. It is the difference between a timeout costing you nothing and a customer getting two one-time codes.

  • Same key, same body: you get the original response back, with an Idempotent-Replay: true header. Nothing is sent twice and nothing is charged twice.
  • Same key, different body: 409 idempotency_key_reused. Your key is not as unique as your code thinks.
  • Same key while the first is still running: 409 idempotency_in_progress. Retry in a moment.

Keys are remembered for 24 hours and are scoped to your workspace. Use something naturally unique to the event, like order-4471-shipped, rather than a random value your retry cannot reproduce.

Reading a message back

shell
curl https://sendrixbackend.exebee.com/v1/messages/msg_c16cead6-8385-481f-9e2e-e146d51f2738 \
  -H "Authorization: Bearer sk_live_..."

Requires messages:read. Useful while you are building, before your webhook exists. Once it does, prefer the webhook: polling every message costs you rate limit for information we would have pushed.

Statuses

StatusMeans
queuedAccepted by us, not yet handed to Meta.
sendingOn its way to Meta right now.
sentMeta accepted it.
deliveredIt reached the handset.
readThe recipient opened it.
failedIt will not arrive. error.code is Meta’s own code.

Treat this list as able to grow. A status you do not recognise should be handled as “still in flight” rather than crashing.

What a send costs

On credits: one, reserved when we accept the message and settled when Meta says what happened. A message that never arrives has its credit returned automatically, the same as a campaign send. If your balance cannot cover it you get 402 insufficient_credits and nothing is queued.

On the all-inclusive plan: nothing is charged, and the send comes off the plan’s allowance for the period instead. Credits are never used, so once the allowance is gone you get 402 plan_allowance_used_up rather than a fallback to the balance.

With no plan at all: 402 plan_required. Reads keep working, so an integration can still list its senders and templates and work out what happened. See Plans and credits.