WhatsApp API
Send and receive WhatsApp messages, manage templates and opt-outs, and run broadcasts from your own code.
The WhatsApp API does everything the dashboard does: send a message, read a conversation, hand one over to a person, manage templates and opt-outs, and run a broadcast. The agent answers incoming messages by default, and your code follows along by polling.
Base URL https://omnidim.io/api/v1 · All routes live under /api/v1/whatsapp.
Before you start
- An API key. Copy it from the dashboard. See Authentication.
- A WhatsApp number connected, in the dashboard or over the API with your Meta access token and WhatsApp Business Account id. See Connect a number below, or Connect a number in the dashboard.
- An approved template for any chat you start yourself, because the 24-hour window is closed until the customer writes to you. See Templates.
- An agent on the number if you want replies answered. See Agent replies.
To see the whole API at a glance, open the WhatsApp reference.
How a conversation flows
- 1
Set up the number
Connect WhatsApp, approve a template, and attach the agent that answers. - 2
Send from your code
POST /whatsapp/messages: free text inside the 24-hour window, or an approved template any time. - 3
The agent answers incoming messages
Replies go out on their own. There is nothing to build for this. - 4
Your integration follows the chat
New messages
Poll GET /whatsapp/messages with after, or conversations with updated_since.
A person steps in
POST take_over to stop the agent, chat, then hand_back.
Your step Done for you
The routes, by task
| Task | Start here |
|---|---|
| Connect your own Meta number | Connect numbers |
| Find your number and what it can send | List numbers |
| Send a text, template, image or document | Send a message |
| Read a chat, and poll it for new messages | List messages |
| The list of chats on a number | List conversations |
| A person takes over, then hands back | Take over |
| Upload an image or document to send | Upload media |
| Templates, and syncing them from Meta | List templates |
| Block someone, or lift a block | List opt-outs |
| A broadcast to a list | Create a campaign |
| Turn calling on or off | Get calling |
Lists come back as { "data": [...], "has_more": true, "next_cursor": "..." }.
Pass limit (1 to 100, default 20) and hand next_cursor back until it is
null.
Connect a number
Connect numbers from your own WhatsApp Business Account with a Meta access token that never expires and has both WhatsApp permissions. A system user token does. Meta credentials shows where to find the token and the account id.
Preview the numbers
API reference: Preview numbers
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/numbers/preview" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"waba_id": "YOUR_WABA_ID", "access_token": "YOUR_META_TOKEN"}'Each number comes back with its phone_number_id and its health. A token that
cannot connect is refused here, with a code that says why (token_expires,
token_missing_permissions or token_invalid).
Connect the ones you want
API reference: Connect numbers
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/numbers" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"waba_id": "YOUR_WABA_ID",
"access_token": "YOUR_META_TOKEN",
"phone_number_ids": ["PHONE_NUMBER_ID"],
"app_secret": "YOUR_META_APP_SECRET"
}'The answer lists each connected number with its id, the id every other route
takes. receiving says whether this request turned replies on. Send
app_secret to turn them on. Find it in your Meta app under App settings >
Basic. Numbers that could not connect are in failed, with the reason.
To change the token or the app secret later, connect the same number again with the new value. It updates the number.
Send a message
Find your number
API reference: List numbers
curl "https://backend.omnidim.io/api/v1/whatsapp/numbers" \
-H "Authorization: Bearer YOUR_API_KEY"{
"data": [
{
"id": 412,
"phone_number": "+15551234567",
"provider": "meta",
"name": "Support",
"status": "active",
"quality_rating": "GREEN",
"messaging_limit": "TIER_10K",
"capabilities": {
"types": ["text", "template", "image", "document"],
"reply_to": true,
"window_enforced": true,
"delivery_status": "push",
"template_sync": true
}
}
],
"has_more": false,
"next_cursor": null
}Keep the id. capabilities.types says what this number can send. A status
other than active means the number cannot send right now.
Send a text, inside the window
API reference: Send a message
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2d9e-0b7a-4c1e-9a55-3e2f8b1d7c40" \
-d '{
"phone_number_id": 412,
"to": "+15557654321",
"type": "text",
"text": "Your appointment is confirmed for Friday at 10 am."
}'The send comes back as 201 with the message, ready to look up again:
{
"id": 98123,
"conversation_id": "412-15557654321",
"sender": "person",
"type": "text",
"text": "Your appointment is confirmed for Friday at 10 am.",
"status": "pending",
"timestamp": "2026-10-18T09:04:11+00:00",
"timeline": [{ "status": "accepted", "at": "2026-10-18T09:04:11+00:00" }]
}A free-form send needs an open 24-hour window, which the customer opens by
messaging you. Outside it, the send is refused with 409 window_closed and the
message asks for a template. For a new chat, or any send after a day of silence,
use a template.
Or start the chat with a template
Find an approved template, then send it by its id. A template works whether or not the window is open, and it can be the first message in a conversation.
curl "https://backend.omnidim.io/api/v1/whatsapp/numbers/412/templates?status=approved" \
-H "Authorization: Bearer YOUR_API_KEY"Each template lists the fields a send must fill:
{
"data": [
{
"id": 3676,
"name": "appointment_reminder",
"language": "en_US",
"category": "UTILITY",
"status": "approved",
"body_preview": "Hi {{1}}, your appointment is on {{2}}.",
"fields": [
{ "key": "1", "label": "Customer name", "kind": "text", "required": true, "example": "Demo User" },
{ "key": "2", "label": "Appointment time", "kind": "text", "required": true, "example": "Friday, 10 am" }
],
"buttons": []
}
],
"has_more": false,
"next_cursor": null
}curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9a2b7c1e-4d6f-4a80-9c33-5e1f2d8b6a07" \
-d '{
"phone_number_id": 412,
"to": "+15557654321",
"type": "template",
"template": { "id": 3676, "params": { "1": "Rahul", "2": "Friday, 10 am" } }
}'A template you have not synced yet will not be in the list. Run Sync templates after you create or edit one in Meta.
Send a file
API reference: Upload media
Upload first, then send by the returned media_id. Uploading costs nothing;
only the send is charged.
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/media" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "phone_number_id=412" \
-F "type=document" \
-F "file=@invoice.pdf"{
"media_id": "kQ8v2mZ0pLc4xR7tN1sW3yB6dF9hJ5aE",
"type": "document",
"mime": "application/pdf",
"filename": "invoice.pdf",
"size": 48213,
"expires_at": "2026-10-19T09:04:11+00:00"
}Then send it with type image or document and a media object. The
media_id works for 24 hours, for that number and type only.
Read and follow messages
There are no event webhooks, so you poll. Two ways, depending on what you need:
- Follow one chat. Call List messages
with
conversation_idandafterset to the newest message id you already have. You get only the messages newer than it, newest first. - Watch the whole inbox. Call
List conversations
with
updated_sinceset to the time of your last call. You get every conversation with a change since then, including new messages, takes over and opt-outs.
curl "https://backend.omnidim.io/api/v1/whatsapp/messages?conversation_id=412-15557654321&after=98123" \
-H "Authorization: Bearer YOUR_API_KEY"To read a message's delivery, call
Get a message. Its timeline
walks through accepted, sent, delivered and read, and a failed message
carries a failure with a reason and whether a retry is safe.
Take over and hand back
The agent owns a conversation by default and answers every incoming message. An API send does not take it over: send a message and the agent keeps replying.
To answer as a person, take the conversation over. The agent goes quiet, and customer messages are still stored and shown but not answered.
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/conversations/412-15557654321/take_over" \
-H "Authorization: Bearer YOUR_API_KEY"Hand back when the
person is done and the agent resumes on the next message. You can also set
take_over: true on a send to do both in one call.
Broadcasts
A campaign sends one approved template to a list. It is always created as a draft:
- Create a campaign with
a
name, aphone_number_idand an approvedtemplate_id. - Add contacts, one
object up to 500 at once. Each needs a
phone_number. Any other key you send can fill a template parameter throughvariable_mapping. - Launch it with a new
Idempotency-Key. Every pending contact is queued once.
Launch checks the plan's monthly broadcast allowance and your wallet balance.
If either runs short, launch answers 402. You can
pause,
resume and
cancel a running
campaign, and read per-contact status with
List contacts.
Scheduling is not offered over the API. In the dashboard the same broadcasts are described at Broadcasts.
Opt-outs
A customer stops your messages by replying STOP, or by turning marketing off in WhatsApp. Both are listed under List opt-outs.
Block a customer yourself with
their number, and lift it when
they ask to come back. A block the customer made themselves cannot be lifted over
the API: the request answers 409 customer_opt_out, because only the customer can
opt back in by messaging you.
Calling
WhatsApp calling lets a customer call the number, and the agent answers. It is off by default, only on Meta Cloud numbers, and must be included in your plan. Read the state and the checks with Get calling, then Turn it on once every blocking check passes. See WhatsApp calls for the product side.
Errors
Every error is { "error", "error_description", "code", "request_id" }, and
X-Request-Id is on every response. code is the specific reason to branch on.
| Status | What it means |
|---|---|
400 | A bad parameter or body. code says which, such as invalid_parameter, invalid_limit or token_expires. |
401 | The API key is missing or wrong. |
402 | Not enough wallet balance, or the monthly broadcast allowance is used up. |
403 | Your plan or role cannot do this. |
404 | No such number, conversation, message, template, campaign or opt-out in your workspace. |
409 | A state conflict: window_closed, opted_out, not_supported, invalid_transition or a reused idempotency key. |
429 | Too many template syncs. Retry-After says how long to wait. |
502 | WhatsApp could not be reached. Nothing was sent or charged. |
503 | WhatsApp is not connected for the number, or is briefly unavailable. |
