Skip to main content
Version: v0.7.6

Send messages from a session

Send text, images, documents, audio, locations, contact cards, reactions, and bulk broadcasts from any connected session over the REST API. Every send is one HTTP call addressed to a session and a recipient.

Prerequisites

All examples use these placeholders. Set them once in your shell:

export BASE="http://localhost:2785/api" # /api is the global prefix; behind your domain + TLS in production
export API_KEY="YOUR_API_KEY" # an operator-or-higher key, from your dashboard
export SESSION="my-session" # the id of a ready session

Address a recipient with a JID

Every send takes a chatId — a JID (Jabber ID), WhatsApp's address for a chat. OpenWA uses two forms:

RecipientJID formExample
A person<phone>@c.us — full international number, digits only, no +6281234567890@c.us
A group<groupId>@g.us120363021234567890@g.us

To message +62 812-3456-7890, the chatId is 6281234567890@c.us. For the other JID dialects and where each appears, see the Glossary.

Confirm a number is on WhatsApp

Resolve a raw number to its canonical JID with GET /api/sessions/{sessionId}/contacts/check/{number} before sending. See the API reference.

Send a text message

POST /api/sessions/{sessionId}/messages/send-text takes a chatId and a text body of up to 4096 characters.

curl -X POST "$BASE/sessions/$SESSION/messages/send-text" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "chatId": "6281234567890@c.us", "text": "Hello from OpenWA!" }'

Every send route returns 201 Created with the WhatsApp message id and a timestamp (epoch seconds, a number):

{ "messageId": "true_6281234567890@c.us_3EB0ABCD", "timestamp": 1719312000 }

Keep messageId — you need it to react to, reply to, or forward this message later.

Send media

The media routes — send-image, send-video, send-audio, send-document, and send-sticker — all share one flat request body (SendMediaMessageDto). There is no nested { image: { url } } wrapper. Provide exactly one media source:

  • url — a public http/https URL that OpenWA fetches server-side, or
  • base64 — raw base64 data, in which case mimetype is required.
FieldRequiredConstraints
chatIdyesRecipient JID.
urlconditionalRequired when base64 is absent. Fetched through an SSRF guard.
base64conditionalRequired when url is absent. Decoded size is checked against the media cap.
mimetypeconditionalRequired with base64 — for example image/jpeg, application/pdf, audio/ogg.
filenamenoMax 255 characters.
captionnoMax 1024 characters. Ignored for audio.

Omitting both url and base64, or sending base64 without mimetype, returns 400.

Image by URL

curl -X POST "$BASE/sessions/$SESSION/messages/send-image" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "6281234567890@c.us",
"url": "https://example.com/image.jpg",
"caption": "Check out this image!"
}'

Image by base64

curl -X POST "$BASE/sessions/$SESSION/messages/send-image" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "6281234567890@c.us",
"base64": "/9j/4AAQSkZJRg...",
"mimetype": "image/jpeg",
"filename": "photo.jpg"
}'

Document

curl -X POST "$BASE/sessions/$SESSION/messages/send-document" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "6281234567890@c.us",
"url": "https://example.com/report.pdf",
"filename": "report.pdf",
"mimetype": "application/pdf"
}'

Audio / voice note

curl -X POST "$BASE/sessions/$SESSION/messages/send-audio" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "6281234567890@c.us",
"url": "https://example.com/voice.ogg",
"mimetype": "audio/ogg"
}'

send-video and send-sticker use the identical body — point url or base64 at a video (video/mp4) or a sticker (image/webp).

Media size cap

All media share one byte cap: 50 MiB (52,428,800 bytes) by default, set by the MEDIA_DOWNLOAD_MAX_BYTES environment variable. It bounds base64 sends, remote-URL downloads, and inbound media alike. A blob whose decoded size exceeds the cap is rejected with 413 Payload Too Large. A remote URL pointing at an internal or blocked address is rejected by the SSRF guard with 400.

Send a location

send-location requires latitude and longitude; description and address are optional.

curl -X POST "$BASE/sessions/$SESSION/messages/send-location" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "6281234567890@c.us",
"latitude": -6.2088,
"longitude": 106.8456,
"description": "Jakarta",
"address": "Central Jakarta"
}'

Coordinates outside the valid latitude/longitude range return 400.

Send a contact card

send-contact requires chatId, contactName, and contactNumber.

curl -X POST "$BASE/sessions/$SESSION/messages/send-contact" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "6281234567890@c.us",
"contactName": "John Doe",
"contactNumber": "628987654321"
}'

React to a message

POST /api/sessions/{sessionId}/messages/react adds or removes an emoji reaction on an existing message. All three fields are required; send an empty emoji to remove a reaction.

curl -X POST "$BASE/sessions/$SESSION/messages/react" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chatId": "6281234567890@c.us",
"messageId": "true_6281234567890@c.us_3EB0ABCD",
"emoji": "👍"
}'

Unlike the send routes, react returns 200 OK with a fixed body:

{ "success": true }

Reply and forward follow the same pattern: reply quotes a prior message ({ chatId, quotedMessageId, text }), and forward copies one between chats ({ fromChatId, toChatId, messageId }). Both return 201. See the API reference.

Send to many recipients

POST /api/sessions/{sessionId}/messages/send-bulk queues a batch and processes it in the background, pacing sends to look natural. It accepts up to 100 messages and returns immediately.

Each item carries a type (text, image, video, audio, or document) and a typed content object. Use {{name}} placeholders in text plus a per-item variables map for mail-merge style substitution.

curl -X POST "$BASE/sessions/$SESSION/messages/send-bulk" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "chatId": "628111111111@c.us", "type": "text", "content": { "text": "Hi {{name}}" }, "variables": { "name": "Alice" } },
{ "chatId": "628222222222@c.us", "type": "image", "content": { "image": { "url": "https://example.com/promo.jpg" }, "caption": "Promo" } }
],
"options": { "delayBetweenMessages": 3000, "randomizeDelay": true, "stopOnError": false }
}'

send-bulk returns 202 Accepted with a batchId and a statusUrl:

{
"batchId": "batch_a1b2c3d4",
"status": "pending",
"totalMessages": 2,
"estimatedCompletionTime": "2026-06-25T09:21:00.000Z",
"statusUrl": "/api/sessions/my-session/messages/batch/batch_a1b2c3d4"
}

options controls pacing: delayBetweenMessages (milliseconds, 1000–60000, default 3000), randomizeDelay (adds 0–2 s of jitter, default true), and stopOnError (halt on the first failure, default false).

Track and cancel a batch

Poll GET /api/sessions/{sessionId}/messages/batch/{batchId} for progress:

{
"batchId": "batch_a1b2c3d4",
"status": "processing",
"progress": { "total": 2, "sent": 1, "failed": 0, "pending": 1, "cancelled": 0 },
"results": [
{ "chatId": "628111111111@c.us", "status": "sent" },
{ "chatId": "628222222222@c.us", "status": "pending" }
]
}

Batch status is one of pending, processing, completed, cancelled, or failed; each result is pending, sent, failed, or cancelled. Cancel a running batch with POST /api/sessions/{sessionId}/messages/batch/{batchId}/cancel; remaining pending items move to cancelled.

Send from the JavaScript SDK

The @rmyndharis/openwa SDK wraps every route above. Construct the client with the server root (no /api suffix) and your key:

import { OpenWAClient } from "@rmyndharis/openwa";

const client = new OpenWAClient({
baseUrl: "http://localhost:2785",
apiKey: process.env.OPENWA_API_KEY!,
});

const result = await client.messages.sendText("my-session", {
chatId: "6281234567890@c.us",
text: "Hello from the OpenWA SDK!",
});

console.log(result.messageId);

client.messages exposes sendImage, sendDocument, sendAudio, sendLocation, sendContact, react, and sendBulk with the same fields as the curl bodies above. See SDK usage.

Common errors

Errors use the standard envelope { statusCode, message, error }. On a 400 validation failure, message is an array of field-level strings. Any body field not listed for a route is rejected with 400 (strict validation).

StatusCauseFix
400 Bad RequestValidation failed, an unknown body field, neither url nor base64, base64 without mimetype, an SSRF-blocked URL, out-of-range coordinates, or the session has no live engine — an unknown or not-yet-started session id both return 400 (Session '...' is not active. Start the session first.), not 404.Check the message array and the field constraints above; confirm the session id is correct and ready.
401 UnauthorizedMissing or invalid X-API-Key, or a key used outside its allowed sessions or IPs.Send a valid key scoped to this session. See Authentication.
403 ForbiddenThe key is valid and in scope but its role is below operator.Use an operator-or-higher key.
404 Not FoundThe batch id on GET batch/{batchId} or POST batch/{batchId}/cancel does not exist. (A missing or not-started session id on a send route returns 400, not 404 — see the row above.)Verify the batchId from the send-bulk response.
413 Payload Too LargeDecoded media exceeds the 50 MiB cap.Compress the file or send a smaller asset.
500 Internal Server ErrorThe send failed at the WhatsApp engine.Confirm the session is connected and retry.

Next steps