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.

POST https://sozuri.net/api/v1/whatsapp/send

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:

SettingWhat 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.

FieldRequiredTypeDescription
projectYesStringThe Sozuri project name that owns the API key making this request. Required on every request — the key and project name together authenticate the call.
toYesStringRecipient in E.164 (2547XXXXXXXX), a full JID (2547...@s.whatsapp.net), or a group JID (1234...@g.us).
textOne ofStringMessage text, or the caption for a media message.
imageUrlOne ofURLPublic URL of an image (with optional text caption).
videoUrlOne ofURLPublic URL of a video (with optional text caption).
audioUrlOne ofURLPublic URL of an audio file. Add "voice": true for a voice note.
documentUrlOne ofURLPublic URL of a document. Optional documentFilename and text.
stickerUrlOne ofURLPublic URL of a .webp sticker.
locationOne ofObject{ latitude, longitude, name?, address? }latitude and longitude must be JSON numbers, not strings.
contactOne ofObject{ fullName, phone, organization? }
mentionsNoArrayJIDs 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"
}
statusMeaning
sentWhatsApp accepted the message from us.
deliveredIt reached the recipient's device (two grey ticks).
readThe recipient opened it (blue ticks). Only if they have read receipts on.
failedWhatsApp 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

HTTPcodeWhen
400INVALID_REQUESTMissing to, nothing to send, or more than one content field.
401UNAUTHORIZEDMissing or invalid API key.
402INSUFFICIENT_BALANCENot enough balance to cover the message. Top up and retry.
409CHANNEL_NOT_CONFIGUREDWhatsApp isn't connected for this project — scan the QR in the dashboard.
409RATE_NOT_CONFIGUREDNo WhatsApp price is set on your account. Contact support — retrying won't clear it.
429RATE_LIMITEDSend rate exceeded. Retry after the seconds given in Retry-After.
502PROCESSING_ERRORThe WhatsApp gateway was unreachable or errored.
503CHANNEL_RECONNECTINGYour 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
  }
}