Skip to content
MultiChats

Developers

A WhatsApp API and a webhook for your own backend

MultiChats exposes two integration points. A REST API lets your code send messages from any connected number with an API key, and a webhook delivers every inbound message and every reply your agents send to an endpoint you control — so your CRM, bot or bookkeeping service reacts to conversations without polling anything or scraping a screen.

Authentication

One header. The key is server-to-server — it carries no user identity and must never be shipped to a browser.

Base URL

https://api.multichats.com/api

Header

X-Api-Key: <your key>

Errors

Real HTTP status codes. 409 means the number is reconnecting, 404 is often a normal answer on lookup routes, and 400 carries a validation message.

Send a text message
curl -X POST https://api.multichats.com/api/accounts/<accountId>/messages \
  -H "X-Api-Key: $MULTICHATS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to":   "233240000001@c.us",
    "type": "text",
    "text": "Your order #4182 has shipped."
  }'

The send is queued and executed by the server that currently owns that number, which is what keeps one number from being driven by two machines at once. You get a queue id back immediately and a delivery receipt when it has actually gone out — so a slow or reconnecting session delays a message instead of losing it.

Endpoint reference

Every route an API key can call. Paths are relative to the base URL above; an account may be addressed by its UUID or by its gateway instance id.

Accounts

An account is one connected WhatsApp number. Routes that need the live session (QR, logout, reconnect) are proxied to the channel engine that currently owns it; the rest read and write this backend's own rows. An account may be addressed by its UUID or by its gateway instance id.

MethodPathWhat it does
GET/accountsList connected numbers, optionally filtered by workspace.
GET/accounts/:idOne account plus a live QR when it is waiting to be linked. The QR rotates roughly every 20 s and is never stored.
POST/accountsRegister a number. `gatewayId` picks which region will hold the session.
DELETE/accounts/:idLog out upstream, then delete the row, its history and its stored session.
POST/accounts/:id/qrAsk the live session for a fresh pairing code.
POST/accounts/:id/reconnectRestart the session without unlinking the number.
POST/accounts/:id/logoutUnlink the number from WhatsApp. Re-linking needs a new QR scan.
POST/accounts/:id/backfillPull recent history for the account's chats.
POST/accounts/:id/read-allMark every conversation on the number as read.
PUT/accounts/:id/gatewayMove the session to another region. Brief offline window, no new QR.
POST/accounts/:id/labelRename the account as it appears in the console.
POST/accounts/:id/send-intervalPacing between outbound messages for this number.
POST/accounts/:id/auto-reject-callsAutomatically decline incoming WhatsApp calls.
POST/accounts/:id/read-receiptsWhether the number sends read receipts.
GET/accounts/:id/eventsOperations log for the number — status changes, QR issued, scanned, reconnect, logout, errors.
GET/gateways/optionsThe regions a new account may be placed in.

Messages

Sending is asynchronous: the message is queued here and executed by the server that currently owns the number, which is what stops one number being driven by two machines. You get a queue id immediately and a delivery receipt on the webhook.

MethodPathWhat it does
POST/accounts/:id/messagesSend text, media, a voice note, a location or a contact card.
POST/accounts/:id/messages/broadcastOne text to many recipients — one queue row each, at bulk priority.
GET/chats/:chatId/messagesHistory for a conversation, keyset-paginated; filter by query text or message type.
GET/accounts/:id/messagesKeyword search across the number's messages.
GET/accounts/:id/changesReplay feed. Everything written since a cursor, including edits, acks and deletions — how a client catches up after being offline.
POST/accounts/:id/messages/revokeDelete a sent message for everyone.
POST/accounts/:id/messages/editEdit a message already sent.
POST/accounts/:id/messages/reactAdd or clear an emoji reaction.
POST/accounts/:id/messages/forwardForward a message to another chat.
POST/accounts/:id/messages/starStar or unstar a message.
POST/accounts/:id/messages/pinPin or unpin a message in its chat.
GET/accounts/:id/messages/outboundThe send queue: what is waiting, sending, sent or failed.
GET/accounts/:id/messages/outbound/statsQueue counts per status.
POST/accounts/:id/messages/outbound/resendRequeue failed or expired sends.
POST/accounts/:id/messages/outbound/clearDrop queued, failed or expired rows. Delivered sends are never cleared.

Chats

Conversation rows live in this backend, so listing and searching them never touches WhatsApp. Chat state that WhatsApp itself owns — read, archive, pin, mute, typing — is proxied to the live session.

MethodPathWhat it does
GET/accounts/:id/chatsConversations by recency, each with a truncated last-message preview.
GET/accounts/:id/chats/lookupFind a chat by its WhatsApp id. A 404 here is a normal answer, not an error.
POST/accounts/:id/chats/openStart a conversation with a number that has never written to you.
POST/accounts/:id/chats/:chatId/readMark a conversation read.
POST/accounts/:id/chats/:chatId/unreadMark it unread again.
POST/accounts/:id/chats/:chatId/archiveArchive or unarchive.
POST/accounts/:id/chats/:chatId/pinPin or unpin the conversation.
POST/accounts/:id/chats/:chatId/muteMute or unmute notifications.
POST/accounts/:id/chats/:chatId/typingShow the typing indicator to the other side.
POST/accounts/:id/chats/:chatId/sync-historyAsk WhatsApp for older messages in this conversation.
DELETE/accounts/:id/chats/:chatIdDelete the conversation and its history.
POST/chats/:chatId/assignAssign the conversation to a staff member.
POST/chats/:chatId/noteAttach an internal note. Never sent to the customer.
POST/chats/:chatId/tagsSet the conversation's tags.

Contacts and groups

WhatsApp now identifies the same person by two different ids — a phone-based one and an opaque one — and they never string-match. These routes resolve between them, so a person is one contact rather than two.

MethodPathWhat it does
GET/accounts/:id/contactsKnown contacts for the number, with both identities where resolved.
GET/accounts/:id/contacts/lookupOne contact by WhatsApp id.
GET/accounts/:id/contacts/groupsWhich groups a person belongs to — matched on either identity.
GET/accounts/:id/groupsThe groups this number is a member of.
GET/accounts/:id/chats/:chatId/membersA group's current roster.
POST/accounts/:id/chats/:chatId/group/subjectRename a group.
POST/accounts/:id/chats/:chatId/group/descriptionSet the group description.
POST/accounts/:id/chats/:chatId/group/participantsAdd, remove, promote or demote members.
POST/accounts/:id/chats/:chatId/group/inviteGet the group's invite link.
POST/accounts/:id/chats/:chatId/group/invite/revokeRevoke the invite link.
POST/accounts/:id/chats/:chatId/group/leaveLeave the group.
GET/contacts/bookAddress book across every number, one row per person.
GET/contact-namesThe shared name book.
PUT/contact-names/:keyRename a person once, everywhere.

Media

Media bytes live in object storage in the region that received them; the API hands out the metadata and a short-lived signed token. Files are content-addressed and immutable, so they cache indefinitely.

MethodPathWhat it does
GET/media/tokenMint a short-lived token an <img> or <video> tag can carry.
GET/media/:idFetch the file. `variant=thumb` returns the thumbnail where one exists.

Workspaces and templates

A workspace owns numbers, staff and the settings that apply to all of them — including where the webhook points.

MethodPathWhat it does
GET/workspacesWorkspaces the caller can see.
POST/workspacesCreate a workspace.
POST/workspaces/joinJoin one with an invite code.
GET/workspaces/:id/membersThe roster.
POST/workspaces/:id/invite/rotateIssue a fresh invite code and retire the old one.
POST/workspaces/:id/webhookWhere message events are delivered.
POST/workspaces/:id/webhook/testFire one probe at the saved URL.
GET/workspaces/:id/accountsNumbers owned by the workspace.
POST/workspaces/:id/accounts/:accountId/assigneesWhich staff may work a number.
GET/templatesCanned replies for the workspace.
POST/templatesCreate one.
PATCH/templates/:idEdit one.
DELETE/templates/:idDelete one.

Webhook events

Everything that happens on a number is posted to your endpoint as it happens — you never poll.

Delivery
POST https://your-backend.example.com/hooks/multichats
X-Webhook-Signature: sha256=<hmac of the exact body bytes>

{
  "event":     "message.received",
  "accountId": "5ea23432-519a-4d38-b377-4ddbfc2ff9ee",
  "timestamp": "2026-08-14T09:12:44.031Z",
  "data": { "from": "233240000001@c.us", "type": "chat", "body": "is it ready?" }
}
EventFired when
message.receivedA customer sent a message to one of your numbers.
message.sentOne of your numbers sent a message — from the console or the API.
message.ackA message you sent was delivered, or read.
message.revokedA message was deleted for everyone.
outbound.statusA queued send finished: executed, failed or expired.
account.statusA number connected, disconnected or needs re-linking.
account.qrA new pairing QR was issued for a number.
group.updatedA group's subject, description or roster changed.

Durable, not best-effort

Deliveries are written to an outbox in the same transaction that records the message, then retried with backoff. A receiver that is down for ten minutes costs latency, not events.

Signed

Each POST carries an HMAC-SHA256 signature over the exact body bytes, so your endpoint can verify it came from your deployment before acting on it.

Both directions

Inbound customer messages and outbound agent replies both arrive, with the operator attributed — so your system sees the whole conversation, not half of it.

What it is built on

A FastAPI service backed by PostgreSQL and a Redis-compatible cache, with one or more regional channel engines driving the WhatsApp sessions and object storage holding media. Everything durable — messages, chats, contacts, the send queue — lives in your database, and the consoles are static bundles that talk to this API over HTTPS and a websocket.