Skip to content
WhatIsUp.dev

Messages

Send a WhatsApp message from a connected channel. Inbound messages are pushed to you as message.received webhooks, and both directions are also stored and readable through the message history routes (see Message history below).

Sending is async — the API returns immediately, delivery state comes back as a webhook.

Send a message

POST/v1/channels/:id/messagesBearer · API key

Which channel states accept a send:

Channel statusResult
connected202, status: "sent" — handed to WhatsApp now.
connecting / reconnecting / disconnected, or pending / qr while re-pairing a channel that was paired before202, status: "queued" — held and sent once the channel connects.
pending / qr on a channel that has never been paired409 channel_unavailable with details.paired: false — scan the QR code first.
logged_out / stopped_by_user / disabled_by_admin / error / failed409 channel_unavailable — reconnect or reset the channel first. details.status says which state it is in.
Schema · Request body
FieldTypeRequiredNotes

The body is flat: type, to and the fields of that type sit side by side at the top level (there is no nested body object). Valid shapes:

// text
{ "type": "text", "to": "5511999999999@s.whatsapp.net", "text": "hello" }
 
// image — mime_type is required for image / video / audio / document
{
  "type": "image",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/photo.jpg",
  "mime_type": "image/jpeg",
  "caption": "look"
}
 
// audio — ptt: true renders a voice note (best with Ogg/Opus)
{
  "type": "audio",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/note.ogg",
  "mime_type": "audio/ogg; codecs=opus",
  "ptt": true
}
 
// document
{
  "type": "document",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/invoice.pdf",
  "mime_type": "application/pdf",
  "filename": "invoice.pdf"
}
 
// sticker (WebP — static or animated)
{
  "type": "sticker",
  "to": "5511999999999@s.whatsapp.net",
  "media_url": "https://example.com/sticker.webp"
}

Stickers must be WebP. mime_type is optional for stickers only and defaults to image/webp.

to is a WhatsApp JID: the international number, digits only (no +), followed by @s.whatsapp.net for a person, or a group id ending in @g.us. A bare number such as "5511999999999" fails validation with 400 invalid_request on this route. (The typed alias POST /v1/channels/:id/messages/:type does accept a bare number and adds the suffix for you.) A @lid id, or a number WhatsApp says is not registered, returns 400 undeliverable_recipient.

curl -sX POST "$WHATISUP_API/v1/channels/8d653c66-e4ff-43ee-97da-3de5ad5680d4/messages" \
  -H "Authorization: Bearer $WHATISUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"text","to":"5511999999999@s.whatsapp.net","text":"hi"}'

Errors this route returns:

Statuserror.codeWhen
400invalid_requestThe body failed validation (details.issues lists why).
400undeliverable_recipientto can't receive WhatsApp messages. details.to echoes it.
402trial_expired / payment_overdueSending is paused on your account until you pick a plan or settle billing.
404channel_not_foundNo channel with that id on your account.
409channel_unavailableThe channel is in a terminal state (see the table above).
429rate_limitedSlow down — see Rate limits.
Schema · Response
FieldTypeRequiredNotes
message_idstringrequired
client_refstringrequired · nullable
status`sent` \| `queued`required
quotedbooleanoptional
warningsarray<object>optional

The response is 202 Accepted, and status discriminates two outcomes:

  • "sent" — the channel was connected and the gateway handed the message to its WhatsApp connection. This is acceptance, not delivery: in message history the message reads pending until WhatsApp's servers acknowledge it, then sent → delivered → read.
  • "queued" — the channel was mid-recovery; we hold the message and retry it under the same message_id until it lands or the channel stops terminally. It shows up in message history right away as pending. If it can never go out — the channel stays offline for more than 2 hours, is logged out or stopped, or the recipient isn't on WhatsApp — you get one message.status webhook with status: "failed" and a machine reason (see payloads), and history shows failed. A send to a connected channel can also come back "queued": while older messages for that channel are still waiting in the queue, a new one lines up behind them rather than overtaking them.

A warnings array appears when there is something to know about a send that still went ahead. Today: new_chat_restricted / new_chat_capped — WhatsApp isn't letting this number start new chats right now and the recipient never wrote to it, so WhatsApp will most likely refuse the message (it then turns failed). The gateway never refuses the send itself; see Keeping numbers healthy.

Both are success. Re-POSTing after a "queued" response creates a second WhatsApp message — unless both requests carry the same Idempotency-Key header, in which case the retry gets the original response back (Idempotency).

Final delivery state arrives as message.status webhooks carrying status: "sent" | "delivered" | "read" | "played" | "failed". There is no separate message.delivered or message.failed event. Correlate on message_id.

Reply to a message (quoted reply)

Add reply_to to any message-bearing send (text, image, video, audio, document, sticker) and WhatsApp renders the original above your message — exactly like swiping to reply in the app.

{
  "type": "text",
  "to": "5511999999999@s.whatsapp.net",
  "text": "We open at 9am on Saturday.",
  "reply_to": { "message_id": "3EB0AA11BB22CC33DD44EE55FF66007788AA" }
}

reply_to.message_id is any message id this channel has seen: the message_id from a message.received or message.sent webhook, the quoted.message_id of an incoming reply, or the id returned by an earlier send. You never have to remember WhatsApp's internal key — the gateway rebuilds it (conversation + author) from its own message store.

The 202 answers whether the citation actually shipped:

{ "message_id": "3EB0…", "client_ref": null, "status": "sent", "quoted": true }
quotedMeaning
absentThe request carried no reply_to (or came back status: "queued" — see below).
trueThe message went out quoting that id.
falseThe id was unknown here — never seen on this channel, aged out of retention, or from a different conversation. The message was still sent, just without the citation.

A bad reply_to never fails a send. Losing a quote is recoverable; losing the message is not. If the citation matters to your flow, check quoted on the response instead of assuming it.

On status: "queued" there is no quoted field: the channel was offline, so the citation is resolved later, when the queued message actually sends. The message.sent webhook carries the verdict in its own quoted field.

Quoting a message from a different conversation is not supported by WhatsApp — the gateway drops the citation (quoted: false) rather than send a key that renders as a dangling quote on the recipient's phone.

Message history

GET /v1/messages (every channel) and GET /v1/channels/:id/messages (one channel) return stored inbound and outbound messages, newest first, with their bodies, media references and the webhook deliveries that carried each one. Outbound rows carry their delivery status (pending, sent, delivered, read, played or failed) and status_updated_at, so you can read a message's current state here as well as receive it as message.status webhooks.

{
  "data": [ { "id": "…", "direction": "out", "status": "delivered", "…": "…" } ],
  "has_more": true,
  "next_cursor": "2026-09-25T12:00:00.000Z",
  "next_cursor_id": "0b8f…"
}

Page with limit (1–100, default 20) and, for the next page, send the previous response's next_cursor as before and next_cursor_id as before_id. When a request returns an empty data, you've reached the end.

What you don't see in this API

  • No single-message GET. There is no GET for one message id — find it in the history routes above, or track it through message.status webhooks.
  • No bulk send. Loop the API; the per-customer rate limiter (API keys → Rate limiting) is your throttle. Stripe-style batching is on the roadmap.
  • No "schedule send". Schedule it on your side and call us at fire time.

We emit message.status with status: "read" when WhatsApp reports the recipient opened the chat, and "played" for voice and video notes — so read receipts are available. The caveat is the recipient's own privacy setting: WhatsApp suppresses the read receipt entirely when they have read receipts turned off, so the absence of read is not proof the message is unread.