Messages
Send a WhatsApp message from a connected channel. Receive-direction messages arrive as message.received webhooks; there's no "list inbox" endpoint — webhooks are the read path.
Send a message
The channel must be connected. Sending while pending / qr / disconnected returns 409 not_connected.
| Field | Type | Required | Notes |
|---|
The body is a discriminated union on type. Valid shapes:
// text
{
"to": "5511999999999",
"body": { "type": "text", "text": "hello" }
}
// image
{
"to": "5511999999999",
"body": { "type": "image", "media_url": "https://...", "caption": "look" }
}
// audio
{
"to": "5511999999999",
"body": { "type": "audio", "media_url": "https://..." }
}
// document
{
"to": "5511999999999",
"body": { "type": "document", "media_url": "https://...", "filename": "invoice.pdf" }
}
// sticker (WebP — static or animated)
{
"to": "5511999999999",
"body": { "type": "sticker", "media_url": "https://example.com/sticker.webp" }
}Stickers must be WebP. mime_type is optional and defaults to image/webp.
to is an MSISDN — international format with no leading +. We accept group JIDs (...@g.us) for completeness, but most workflows are 1:1.
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 '{"to":"5511999999999","body":{"type":"text","text":"hi"}}'| Field | Type | Required | Notes |
|---|---|---|---|
| message_id | string | required | |
| client_ref | string | required · nullable | |
| status | `sent` \| `queued` | required |
The response is 202 Accepted, and status discriminates two outcomes:
"sent"— the channel was connected and WhatsApp accepted the message."queued"— the channel was mid-recovery; we hold the message and retry it under the samemessage_iduntil it lands or the channel stops terminally.
Both are success. Re-POSTing after a "queued" response creates a second
WhatsApp message — there is no request de-duplication.
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.
What you don't see in this API
- No message-status GET. There is no "poll this id for its current state" route — delivery state is push-only, via
message.statuswebhooks. Message history, however, is readable:GET /v1/messagesandGET /v1/channels/:id/messagesreturn stored inbound and outbound rows with their bodies, media references and the webhook deliveries that carried each one. - 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.