Sending messages

One endpoint covers both channels and every message type. It returns three different success shapes, which is the first thing worth knowing about it.

Choosing a channel

Every send names a channel, and the choice changes what is allowed rather than just how it is routed. These are WhatsApp's rules, not ours.

Capabilitywhatsapp_cloudwhatsapp_web
Service window24 hoursNone
TemplatesYesNo
BillingPer template messagePer session
Max text length4,096 characters65,536 characters
Max media size16 MB64 MB
Throughput80/second1/second
  • Cloud API — Meta's official channel. Free-form messages only within 24 hours of the customer's last message; an approved template is the only way through a closed window.
  • WhatsApp Web — a real account paired by QR code. No window and no templates, but one message a second, and sends come back queued rather than sent because a pacing queue decides when they actually go.

Read the live values for your own numbers from GET /api/channels — a Cloud number on a lower Meta tier reports a lower throughput than the table above.

The endpoint

POST/api/messages/send
Auth: API key (messages:send) or session

Blocked while a subscription is lapsed. No plan feature gate — sending is not an add-on.

cURL
curl -X POST https://api.wpai.co.in/api/messages/send \
  -H "X-API-Key: $WPAI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4471-shipped" \
  -d '{ "to": "919876543210", "type": "text", "text": "Your order has shipped." }'

Request body

FieldTypeRulesDescription
torequiredstringmin 5 charsDestination number. Normalised to digits, so +91 98765 43210 is fine.
channel'whatsapp_cloud' | 'whatsapp_web'default 'whatsapp_cloud'Which channel carries it. Decides which of the fields below apply. Omit on a key bound to a channel and its own is used.
type'text' | 'template' | 'image' | 'video' | 'document' | 'audio'default 'text'What kind of message. `template` is Cloud API only.
wabaAccountIdstringCloud APIWhich connected number sends it. Optional when your key is bound to one, or when the account has only one connected — otherwise required.
phoneNumberIdstringCloud APIThe same choice by Meta’s own id for the number, which you already hold. An alternative to `wabaAccountId`, not an addition.
channelConnectionIdstringany channelWhich connection sends it, by the id shown on the Channels page. Same rule as above.
textstringmax 4096The body. Required for `type: text`.
mediaUrlstring (URL)media typesA publicly reachable URL. Takes precedence over `base64`.
base64stringmedia types, min 16 charsRaw or data-URL encoded bytes. Stored first, then sent by link.
filenamestringmax 255Shown to the recipient for a document.
captionstringmax 1024Text alongside media. Becomes the stored body of the message.
templateIdstringtype: templateAn APPROVED template belonging to this organization.
variableMappingRecord<string, string>type: templateFills the template’s {{1}}, {{2}} placeholders. See Template messages.
headerMediaIdstringtype: templateA Meta media_id for an image, video or document header, from `POST /api/campaigns/media`. Reusable for about 30 days.
headerMediaUrlstring (URL)type: templateA public http(s) URL Meta fetches for the header. No upload step. Ignored when `headerMediaId` is given.
Which fields apply depends on `channel` and `type`. The per-type pages below show complete payloads.

Which fields apply depends on the two above them

channel and type decide the rest. A field that does not apply is ignored rather than rejected — sending templateId with type: "text" does not error, it just sends the text. The one exception is header media: headerMediaId or headerMediaUrl on a template whose header is not an image, video or document is a 400, because silently dropping it would send a header the template cannot fill.

Three success shapes

The channel and the deduplication path each return a different body. Branch on the presence of a field, not on the status code.

Cloud API — sent

201
{
  "success": true,
  "messageId": "68cf1a…",        // our id for this message
  "metaMessageId": "wamid.HBg…",  // Meta's id, echoed by delivery webhooks
  "status": "sent"
}

WhatsApp Web — queued

201
{
  "ok": true,
  "channel": "whatsapp_web",
  "status": "queued",
  "messageId": "68cf1a…"
}

No provider id yet, because it has not gone out. It arrives on the message.status webhook.

Either channel — already sent

200
{
  "ok": true,
  "channel": "whatsapp_cloud",
  "status": "sent",
  "messageId": "68cf1a…",
  "deduped": true
}

Note the 200 rather than 201 — nothing was created.

Retrying safely

Pass an Idempotency-Key header with a value that identifies the message in your own system. A repeat with the same key returns the original result instead of sending again.

  • The key is scoped to your organization, so it only has to be unique to you.
  • It is stored only on a successful send, so retrying after a network failure or a 5xx does the right thing.
  • It is honoured on this endpoint and no other. Every other call in these docs will act on a retry, so guard those on your own side.

Quota and wallet

Two separate meters, and a send may hit either.

  • Plan allowance — one message is reserved before dispatch, on both channels. Exhausted returns 402 naming what is left and offering a message pack. Cloud API plans usually report an unlimited allowance, because they are metered by money instead.
  • Wallet — debited only on Cloud API template sends. Never on text, never on media, never on WhatsApp Web. Checked before, debited after.

A failed send costs nothing

The wallet is debited after Meta accepts the message, and the allowance is released when a dispatch fails. You are not charged for a rejection.

When a send is refused

Errors are JSON with an error string, and sometimes a code to branch on. The message is written for a person and may be reworded; the code will not be.

Error body
{ "error": "Template is not APPROVED by Meta" }
StatusMeans
400The request is wrong, or the channel will not carry it — a closed 24-hour window is the common one. Also returned when your account has several channels connected and the request names none (`channel_ambiguous`); the message lists the ids to choose from.
401No key, a malformed key, or a revoked one.
402Out of message allowance, or too little wallet balance for a template send.
403Your key is bound to a different channel than the one requested (`channel_binding_mismatch`). Omit the channel to use the key’s own.
404The number, template or session you named is not on your account.
409That channel cannot send right now — paused after failing deliveries (`channel_degraded`) or no longer connected (`channel_unavailable`). A send is never moved to another number on its own.
429Rate limited.
502Meta or the WhatsApp Web service rejected it. The message carries their words.
503The WhatsApp Web service is unreachable. Always transient — retry.
A 5xx is safe to retry unchanged; a 4xx is not, until you change something.

Validation errors here name no field

Most endpoints return a details array with the offending path. This one collapses every schema failure into a flat 400 reading Invalid request body parameters. If a send is rejected and you cannot see why, check the field table above.

By message type