Official WhatsApp & one API for both channels
Connect a number through Meta's WhatsApp Business Platform — your verified business name, message templates, no QR code to keep alive — and send on it, or on your QR-code number, through one endpoint. Sozuri picks the right number for every message.
- Connect an official number
- The 24-hour window and templates
- Send a message
- Which number a message leaves from
- Response
- List your channels
- Templates
- Receiving messages
- Delivery & read receipts
- Rate limits
- Pricing
- Error responses
Connect an official number
In the dashboard open Project → WhatsApp → Official WhatsApp (Meta) and press Connect with Meta. Meta's own sign-in window opens: log in with Facebook, choose or create your business portfolio and WhatsApp Business Account, and verify the phone number by SMS or voice call. Sozuri finishes the setup and the number appears on the page, ready to send.
You need admin access to the business portfolio, and a payment method on the WhatsApp Business Account — Meta bills its own fees to you directly (see Pricing).
A number on the official platform can't also be linked on the QR-code channel. Sozuri ignores the QR copy of every message, so nothing is answered twice.
The 24-hour window and templates
On the official channel you may send any message to a customer who has written to that number in the last 24 hours. Outside that window only an approved template can be sent — it is how a conversation is started or restarted. Create templates under Official WhatsApp → Templates (or with the API); Meta reviews each one. A free-form send outside the window is refused before it reaches Meta, with the time the window closed.
Send a message
POST /api/v1/whatsapp/messages works for both WhatsApp channels. It accepts the
official channel's typed shape, and the exact body of /v1/whatsapp/send —
an integration written for the QR channel works unchanged.
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | The project name that owns the API key. |
| to | Yes | String | International format, country code first, no + or leading 0 (254700000000). A QR-channel JID (...@s.whatsapp.net, ...@g.us) or an official-channel user id (KE.1234...) also works — each exists on one channel only. |
| type | For the typed shape | String | text, image, video, audio, document, sticker, location, contacts, interactive, reaction or template, plus an object of the same name: "text": {"body": "…"} (or just "text": "…"), "image": {"link": "https://…", "caption": "…"}, "audio": {"link": "https://….ogg", "voice": true} (a voice note: OGG/OPUS), "template": {"name": "order_ready", "language": "en", "params": {"body": ["Amina"]}}. |
| text, imageUrl, … | For the QR shape | — | The /v1/whatsapp/send fields: text plus at most one media, location or contact field. A contact card and a voice note (voice: true) go out on either channel. |
| channel | No | String | qr or meta to choose the channel yourself. |
| from | No | String | One of your numbers, when you have several — it also decides the channel. |
| replyTo | No | String | The id of the message you are answering; WhatsApp shows it quoted above yours. Works on both channels. On the QR channel the id must be a message of the same chat, or the send is refused with MESSAGE_NOT_FOUND. |
A template (starts a conversation)
curl -X POST https://sozuri.net/api/v1/whatsapp/messages \
-H "Authorization: Bearer $SOZURI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "project": "your_project_name", "to": "254700000000", "type": "template",
"template": { "name": "order_ready", "language": "en", "params": { "body": ["Amina", "A-1042"] } } }'
A reply (inside the 24-hour window)
curl -X POST https://sozuri.net/api/v1/whatsapp/messages \
-H "Authorization: Bearer $SOZURI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "project": "your_project_name", "to": "254700000000", "text": "Your order is on its way" }'
Ask for their location (inside the 24-hour window)
Shows a Send location button. Their answer arrives on your webhook as a location message.
{ "project": "your_project_name", "to": "254700000000", "type": "interactive",
"interactive": { "type": "location_request_message",
"body": { "text": "Where should we deliver?" },
"action": { "name": "send_location" } } }
Open a Flow (a form inside WhatsApp)
Build and publish the Flow in WhatsApp Manager first, then open it by flow_id or
flow_name. flow_cta is the button text (30 characters at most); with
"flow_action": "navigate", name the first screen. The customer's answers arrive
on your webhook as "type": "interactive" with reply.type flow and the
answers in reply.response (JSON text).
{ "project": "your_project_name", "to": "254700000000", "type": "interactive",
"interactive": { "type": "flow",
"body": { "text": "Book your appointment" },
"action": { "name": "flow", "parameters": {
"flow_message_version": "3", "flow_id": "1234567890", "flow_cta": "Book now",
"flow_action": "navigate", "flow_action_payload": { "screen": "WELCOME" } } } } }
Which number a message leaves from
With both channels connected, the first rule that applies decides:
channelorfrom, when you send them.- An address that exists on one channel only (a JID on QR, a user id on the official channel).
- A template → the official channel, the only one with templates.
- A reply → the number the customer last wrote to in the past 24 hours. Automated answers (the AI agent, keyword rules, workflows) always leave from the number the message arrived on.
- Your project's default, set on the Official WhatsApp page.
- Otherwise the QR channel — a free-form message to somebody who has not written would be refused on the official one.
The response says which channel carried the message and why (channel, routed).
Response
{
"status": "accepted",
"channel": "meta",
"routed": "template",
"message": {
"id": "wamid.HBgLMjU0NzAw...",
"to": "254700000000",
"from": "254711000000",
"type": "template",
"status": "pending"
}
}
message.id is the key delivery and read receipts arrive under. On the
official channel an accepted message is pending: sent, delivered and
read follow as Meta reports them.
List your channels
GET /api/v1/whatsapp/channels?project=your_project_name returns what the project has, and
what each channel can do: the message types it sends, when free-form messages are allowed,
and whether it has templates. Add &to=254722000000 and each official number also says
whether that customer's 24-hour window is open, so you know before sending whether only a template will go.
{
"default": null,
"channels": [
{ "channel": "qr", "available": true, "status": "connected", "number": "254700000001",
"sends": ["text", "image", "video", "audio", "document", "sticker", "location", "contacts", "reaction"],
"freeForm": "always", "templates": false },
{ "channel": "meta", "available": true,
"sends": ["text", "image", "video", "audio", "document", "sticker", "location", "contacts", "interactive", "reaction", "template"],
"freeForm": "within 24 hours of the customer's last message", "templates": true,
"numbers": [
{ "number": "254711000000", "phoneNumberId": "1098...", "verifiedName": "Duka Bora", "connected": true,
"quality": "GREEN", "coexistence": false,
"window": { "open": true, "closesAt": "2026-11-09T10:12:00+03:00" } } ] }
]
}
window appears only when you pass to.
Templates
GET /api/v1/whatsapp/templates lists your templates and their review status;
POST /api/v1/whatsapp/templates submits one for Meta's review (name,
language, category of marketing, utility or
authentication, and its body). Only an APPROVED template can be sent.
Receiving messages
Set Your webhook for this number on the Official WhatsApp page. Every message the number
receives is POSTed to it. It is the same envelope as the QR channel's, with the same fields in the same
order; only fromUserId and the reply object are this channel's alone:
{
"event": "message.received",
"channel": "meta",
"from": "254722000000",
"to": "254711000000",
"chatId": "254722000000",
"profileName": "Amina",
"message": { "id": "wamid.HBgL...", "type": "text", "text": "Is my order ready?", "fromMe": false },
"timestamp": "2026-09-27T09:06:40+00:00"
}
to is which of your numbers was written to. A customer who uses a WhatsApp username
may arrive with fromUserId (e.g. KE.1234…) — reply to from through this
API and it reaches them either way. Media arrives with a media object once Sozuri holds a copy of
the file (its url), and a reply carries message.replyTo. Requests are signed exactly
like every Sozuri callback — X-Sozuri-Signature, secret under
Manage API → Callback URLs → Webhook security.
Some message types add one object, and only those types carry it:
message.type | Object | What is in it |
|---|---|---|
location | location | latitude, longitude, and name, address when the pin has them. Same shape as the QR channel. |
contacts | contacts | A list of { "fullName", "phone" }. Same shape as the QR channel. |
interactive, button | reply | What the customer tapped: id is the id you gave the button or list row (or a template quick reply's payload), title what they saw. type is button_reply, list_reply, quick_reply, or flow for a Flow's answers (JSON text in response). |
reaction | reaction | messageId (your message they reacted to) and emoji; an empty emoji means they removed it. Automations never answer a reaction. Same shape as the QR channel. |
message.text still carries words for each of them (the tapped title, the emoji, a described
location or contact), so a receiver that only reads text keeps working. Route on reply.id, not
the title: the title is whatever the customer's language and your template say.
Delivery & read receipts
Receipts always update the dashboard and the inbox. They are not POSTed to your webhook at the moment. When forwarding is switched on, each one arrives once per status, like this:
{
"event": "message.status",
"channel": "meta",
"messageId": "wamid.HBgL...",
"status": "delivered",
"to": "254722000000",
"from": "254711000000",
"timestamp": "2026-09-27T09:07:02+00:00"
}
A failed message carries reason, in words, e.g. undeliverable.
Rate limits
Each channel keeps its own per-project limit, and /v1/whatsapp/messages draws on the same
allowance as the channel-specific endpoint: a message routed to QR counts against the QR channel's (deliberately
small) limit, one routed to the official channel against the official one. Separately, Meta lets a number send
80 messages a second — 20 for a number shared with the WhatsApp Business app — and a burst beyond that is
answered 429 throughput_exceeded before it reaches Meta. Both are safe to retry.
Pricing
Meta bills its fee to your WhatsApp Business Account's payment method, per delivered message, by category: marketing, utility and authentication templates, and — from 1 October 2026 — service replies (each number has 1,000 free service messages a month) and utility templates sent inside the 24-hour window. Messages inside a free entry point window (a customer who reached you from a click-to-WhatsApp ad) are free for 72 hours. Sozuri's own per-message fee is taken from your project balance when Meta reports the message delivered, at the rate for the category Meta reports; nothing is charged for a message that was not delivered.
Each connected number may also carry a Sozuri monthly fee. When it does, the Number tab
shows it before you connect, along with the free trial that comes first. We email you when the trial
starts, halfway through it, and before it ends. It is taken from your project
balance (postpay accounts: added to the invoice). If a month cannot be taken, sending from that number
pauses — 402 channel_fee_unpaid — until it is paid: within the hour of a top-up, or straight
away with Pay now on the Number tab. Messages sent to the number are still received.
Error responses
Errors are { "error": { "code", "message", "status", "channel"? } }:
| Status | Code | What to do |
|---|---|---|
| 409 | not_connected, channel_unavailable | The channel you need is not connected on this project. |
| 422 | window_closed | Send an approved template; error.window says when the window closed. |
| 422 | template_not_found, template_not_approved, template_parameters | Check the template's name, language and values. |
| 422 | unsupported_on_qr, unsupported_on_meta | That kind of message exists on the other channel only. |
| 422 | address_not_on_channel | The channel or from you named cannot reach that address: a …@s.whatsapp.net, @lid or @g.us address exists only on the QR channel, a user id (KE.…) only on the official one. Leave both out and the address decides. |
| 422 | from_required | The project has several official numbers: say which in from. |
| 402 | insufficient_balance | Top up the project. |
| 402 | channel_fee_unpaid, CHANNEL_FEE_UNPAID | The number's monthly fee could not be taken, so it is paused. Top up, or press Pay now on the WhatsApp page. Nothing was sent. |
| 429 | rate_limited, RATE_LIMITED, throughput_exceeded | Slow down and retry. |
| 503 | channel_off, channel_reconnecting | Temporary — retry shortly. |