Skip to content
WhatIsUp.dev

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.

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

Send a message

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

The channel must be connected. Sending while pending / qr / disconnected returns 409 not_connected.

Schema · Request body
FieldTypeRequiredNotes

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"}}'
Schema · Response
FieldTypeRequiredNotes
message_idstringrequired
client_refstringrequired · 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 same message_id until 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.status webhooks. Message history, however, is readable: GET /v1/messages and GET /v1/channels/:id/messages return 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.