Send & receive WhatsApp
Send WhatsApp messages from a number you connect by scanning a QR code — no templates, no business verification. Once connected, drive it from the API for OTPs, order updates and receipts, and receive replies, delivery and read receipts on your own webhook.
- Connect a number
- Channel settings
- Request parameters
- Sample request
- Response
- Rate limits
- Receiving messages
- Delivery & read receipts
- Group chats
- Error responses
Connect a number
WhatsApp is enabled per project. In the dashboard open Project → WhatsApp and click Request WhatsApp. Once our team provisions your channel you'll get an email; return to that page, scan the QR code with WhatsApp (Settings → Linked devices → Link a device), and the number is live for sending and receiving. One number per project.
Your number stays linked until you unlink it in the phone's Linked devices screen,
or you tap Reset in the dashboard. A brief network drop only makes the channel
reconnecting — it does not unlink you, and it does not need a new QR scan. Sends during that
window return 503 CHANNEL_RECONNECTING, which is safe to retry.
Channel settings
Project → WhatsApp carries three switches, each off by default:
| Setting | What it does |
|---|---|
| Inbound webhook URL | Where we POST messages your number receives. You can add or change it at any time, not just when requesting the channel. |
| Send read receipts | Marks inbound messages as read (blue ticks) once we've accepted them. Off means senders see one grey tick, exactly as if nobody had opened the chat. |
| Group messages | Opt in to receiving group messages. See Group chats — even when on, we only forward a group message that @mentions your number. |
Request parameters
The request body is JSON. Provide project and to plus exactly one content field. text doubles as the caption for image, video and document. Media is sent by public URL — there is no upload step.
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | The Sozuri project name that owns the API key making this request. Required on every request — the key and project name together authenticate the call. |
| to | Yes | String | Recipient in E.164 (2547XXXXXXXX), a full JID (2547...@s.whatsapp.net), or a group JID (1234...@g.us). |
| text | One of | String | Message text, or the caption for a media message. |
| imageUrl | One of | URL | Public URL of an image (with optional text caption). |
| videoUrl | One of | URL | Public URL of a video (with optional text caption). |
| audioUrl | One of | URL | Public URL of an audio file. Add "voice": true for a voice note. |
| documentUrl | One of | URL | Public URL of a document. Optional documentFilename and text. |
| stickerUrl | One of | URL | Public URL of a .webp sticker. |
| location | One of | Object | { latitude, longitude, name?, address? } — latitude and longitude must be JSON numbers, not strings. |
| contact | One of | Object | { fullName, phone, organization? } |
| mentions | No | Array | JIDs to @mention, e.g. ["254700000000@s.whatsapp.net"]. Only meaningful when to is a group; write the display @254700000000 into text yourself. |
Sample request
Required headers
POST /api/v1/whatsapp/send
Content-Type: application/json
Authorization: Bearer Your_Project_API_KEY
Text (e.g. an OTP)
curl -X POST https://sozuri.net/api/v1/whatsapp/send \
-H "Authorization: Bearer $SOZURI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "project": "your_project_name", "to": "254700000000", "text": "Your OTP is 123456" }'
Image with a caption
curl -X POST https://sozuri.net/api/v1/whatsapp/send \
-H "Authorization: Bearer $SOZURI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "project": "your_project_name", "to": "254700000000", "imageUrl": "https://example.com/receipt.png", "text": "Your receipt" }'
Response
200 OK — the same shape for every message type:
{
"status": "sent",
"messageId": "BAE5F...",
"to": "254700000000",
"timestamp": "2026-07-19T10:30:00.000Z"
}
messageId is WhatsApp's own id. Keep it — it is the key that
delivery and read receipts arrive under.
Rate limits
Sends are limited per project, and the limit applies to your API key however many
servers you call from. Exceeding it returns 429 with a
Retry-After header; wait that many seconds and retry.
The channel is built for conversational and transactional traffic — OTPs, order updates, receipts, support replies. It is not a broadcast channel. Unsolicited bulk messaging from a QR-linked number is the fastest way to have that number banned by WhatsApp, and a ban is not something we or you can appeal. The current limit is shown on Project → WhatsApp; talk to us before designing anything that needs a higher one.
Receiving messages
Set an inbound webhook URL on Project → WhatsApp. Each message your number receives is POSTed to that URL:
{
"event": "message.received",
"from": "254700000000",
"chatId": "254700000000@s.whatsapp.net",
"message": { "id": "ABC", "type": "text", "text": "hi", "fromMe": false },
"timestamp": "2026-07-19T10:30:00.000Z"
}
from is the sender's number where WhatsApp gives us one. WhatsApp increasingly
addresses chats by a privacy id instead, so from can be a
...@lid JID — still a valid address to reply to, but not a phone
number, so don't store it in a phone-number column.
Media (image/video/audio/document/sticker) arrives with a media object carrying a
download url and the caption in message.text. A file too large to store
arrives as metadata with "truncated": true and no url.
A reply carries message.replyTo with the id of the message being answered.
Every request is signed with the same scheme as your delivery callbacks:
X-Sozuri-Signature: t=<unix>,v1=<hmac>, where the HMAC is
hmac_sha256(signing_secret, "<t>.<raw body>"). Hash the raw
body before parsing it as JSON, take your secret from
Manage API → Callback URLs → Webhook security, and respond
2xx quickly — the reply is not read.
Delivery & read receipts
WhatsApp reports what happened to each message you sent. Those receipts always update the dashboard and the chat inbox; ask support to enable status callbacks and they are also POSTed to your inbound webhook URL, signed identically:
{
"event": "message.status",
"messageId": "BAE5F...",
"status": "read",
"to": "254700000000",
"timestamp": "2026-07-19T10:30:04.000Z"
}
status | Meaning |
|---|---|
sent | WhatsApp accepted the message from us. |
delivered | It reached the recipient's device (two grey ticks). |
read | The recipient opened it (blue ticks). Only if they have read receipts on. |
failed | WhatsApp could not deliver it. |
Status only ever moves forward: pending → sent → delivered →
read. Receipts can arrive out of order, and a late delivered after a
read is discarded rather than moving the message backwards — so you can apply each
event as you get it. read is not guaranteed: many people turn read receipts off, and
those messages simply stop at delivered. Treat delivered, not
read, as your success signal.
Group chats
Group messages are off until you turn on Group messages on Project → WhatsApp. Even then we forward a group message only when your number is @mentioned in it. Everything else said in the group is ignored and never reaches your webhook.
That is deliberate. A busy group would otherwise fire your webhook — and, with automation on, an automated reply — on every message, which is both a bill you didn't ask for and the behaviour that gets numbers banned. Mention-only means the bot speaks when spoken to.
A group message adds a group object and points chatId at the group:
{
"event": "message.received",
"from": "254700000000",
"chatId": "120363000000000000@g.us",
"group": {
"id": "120363000000000000@g.us",
"subject": "Ops team",
"participant": "254700000000@s.whatsapp.net"
},
"message": { "id": "ABC", "type": "text", "text": "@254711000000 what is the ETA?", "fromMe": false },
"timestamp": "2026-07-19T10:30:00.000Z"
}
from / group.participant is who spoke; chatId is where the
answer belongs. To reply into the group, send to the group JID and pass
mentions if you want to tag someone:
curl -X POST https://sozuri.net/api/v1/whatsapp/send \
-H "Authorization: Bearer $SOZURI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "project": "your_project_name",
"to": "120363000000000000@g.us",
"text": "@254700000000 ETA is 14:30",
"mentions": ["254700000000@s.whatsapp.net"] }'
Billing: a group message that mentions you is billed as one inbound message; one that doesn't is not billed at all, because we never receive it. A reply to a group is billed as one outbound message no matter how many people are in the group.
Error responses
| HTTP | code | When |
|---|---|---|
| 400 | INVALID_REQUEST | Missing to, nothing to send, or more than one content field. |
| 401 | UNAUTHORIZED | Missing or invalid API key. |
| 402 | INSUFFICIENT_BALANCE | Not enough balance to cover the message. Top up and retry. |
| 409 | CHANNEL_NOT_CONFIGURED | WhatsApp isn't connected for this project — scan the QR in the dashboard. |
| 409 | RATE_NOT_CONFIGURED | No WhatsApp price is set on your account. Contact support — retrying won't clear it. |
| 429 | RATE_LIMITED | Send rate exceeded. Retry after the seconds given in Retry-After. |
| 502 | PROCESSING_ERROR | The WhatsApp gateway was unreachable or errored. |
| 503 | CHANNEL_RECONNECTING | Your number is still linked but the connection is re-establishing. Retry in a few seconds. |
Errors carry the code in the body as well as the HTTP status:
{
"error": {
"code": "CHANNEL_RECONNECTING",
"message": "The WhatsApp connection is re-establishing. Your number is still linked — retry in a few seconds.",
"status": 503
}
}