Send messages
Send text, media albums, 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 | exactly one of media.data or media.url | media.mimetype, media.caption (image/video/document), media.filename (document), replyTo, mentions |
album | medias with 2–10 image/video items | caption, replyTo, mentions |
A contact is a card. Give it { name, phone }, or supply a raw vcard string
instead.
Media
Media is sent from exactly one source:
media.data— the file's bytes, base64-encoded. Adata:URI (e.g. what a browser'sFileReader.readAsDataURLproduces) is accepted too; its MIME type is used when you don't setmedia.mimetype.media.url— a public HTTP(S) URL for the gateway to download before uploading to WhatsApp.
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!"
}
}URL media uses the same shape:
{
"type": "image",
"to": "628123456789@s.whatsapp.net",
"media": {
"url": "https://example.com/photo.jpg",
"caption": "Here you go!"
}
}The same shape sends video, audio, document, and sticker — change type
(and add media.filename for a document).
Albums
An album groups 2–10 images or videos under one optional caption. Put the caption
at the top level; WhatsApp supports one caption for the grouped album. Every item
may use either base64 data or a public url, and the two source forms can be
mixed. Each item is limited to 16 MiB and the decoded album total is limited to
64 MiB.
{
"type": "album",
"to": "628123456789@s.whatsapp.net",
"caption": "Trip photos",
"medias": [
{ "type": "image", "data": "iVBORw0KGgoAAAANSUhEUgAA...", "mimetype": "image/png" },
{ "type": "image", "url": "https://example.com/photo.jpg" }
]
}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. For URL sends, the outbox stores the URL for retry, not the downloaded
file. 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.