Send messages
Send text, polls, locations, and contact cards through one REST endpoint, with idempotent retries and per-session rate limits.
Sending is one POST to a session's messages endpoint. You need a session that is
already connected (its status is working). Every request body has two required
fields:
type— what kind of message to send.to— the recipient's JID. A JID is WhatsApp's address for a chat: a phone JID like628123456789@s.whatsapp.netfor a one-to-one chat, or a group JID for a group.
The other fields depend on type.
curl -X POST https://your-gateway/api/v1/sessions/{id}/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"text","to":"628123456789@s.whatsapp.net","text":"Hello"}'The reply is a SendResult: the WhatsApp message id (messageId), the delivery
status, and a timestamp. The full request and response shapes are in
send a message.
Message types
type | Required fields | Optional fields |
|---|---|---|
text | text | mentions (a list of JIDs to tag), replyTo (a message id to quote) |
poll | name, options | selectableCount — how many options one voter may pick. Votes come back as poll.vote events. |
location | latitude, longitude | name — a label for the pin |
contact | contact | — |
image, video, audio, document, sticker | media.data | media.mimetype, media.caption (image/video/document), media.filename (document), replyTo, mentions |
A contact is a card. Give it { name, phone }, or supply a raw vcard string
instead.
Media
Media is sent inline: put the file's bytes, base64-encoded, in media.data. The
gateway decodes them, uploads them to WhatsApp, and sends the message — no URL
fetching, so the request is self-contained. A data: URI (e.g. what a browser's
FileReader.readAsDataURL produces) is accepted too; its MIME type is used when
you don't set media.mimetype (otherwise the type is detected from the bytes).
The decoded file must be 16 MiB or smaller. mimetype is optional but
recommended; filename shows on documents; caption shows under image, video,
and document messages.
{
"type": "image",
"to": "628123456789@s.whatsapp.net",
"media": {
"data": "iVBORw0KGgoAAAANSUhEUgAA...",
"mimetype": "image/png",
"caption": "Here you go!"
}
}The same shape sends video, audio, document, and sticker — change type
(and add media.filename for a document).
The uploaded bytes are not retained: the gateway holds them only long enough to
deliver the message (for an async send, until the queued row is drained), then
drops them. The stored message keeps the caption and media metadata, never the
file content.
A poll request looks like this:
{
"type": "poll",
"to": "628123456789@s.whatsapp.net",
"name": "Lunch?",
"options": ["Pizza", "Sushi", "Salad"],
"selectableCount": 1
}Change or reference a sent message
Once a message exists, these endpoints act on it by its message id:
| Action | Endpoint |
|---|---|
| Edit the text | edit a message |
| Delete for everyone | revoke a message |
| React with an emoji | add / remove a reaction |
| Forward it to another chat | forward a message |
| Vote on a poll | vote |
Sync vs. async
By default the send is synchronous: the call blocks until WhatsApp
acknowledges the message, then returns 200 with the messageId and status.
Add ?async=true to enqueue the send instead. You get 202 right away, and the
final delivery status arrives later as a message.status
event.
Retrying safely (idempotency)
Send the same request twice — say, after a network timeout — and you risk sending
the message twice. To prevent that, pass an Idempotency-Key header with a value
you choose. If a request with the same key has already run, the gateway returns
the original result instead of sending again.
The key is scoped to your organization, so the same key reused across sessions in the same org counts as the same request.
Rate limits
Each session has two send budgets: a per-minute cap and a per-hour cap. The
defaults are 20 per minute and 200 per hour (configurable per session; the
server-wide defaults come from DEFAULT_RATE_PER_MIN and
DEFAULT_RATE_PER_HOUR).
What happens when you go over depends on the mode:
| Mode | Over the limit |
|---|---|
| Sync (default) | Returns 429. The error includes retryAfterSeconds. |
Async (?async=true) | The message stays queued and goes out once the budget refills. |
Sending fast or in bulk is exactly the behavior WhatsApp bans numbers for. The rate limits, plus the typing and read-receipt simulation, exist to keep traffic looking human. Keep volume low and pacing natural.