WhatsAble API v1
The public REST API of WhatsAble. Send WhatsApp messages and manage templates, contacts, broadcasts and AI bots from your own code.
What the API does
The API gives your code the same WhatsApp Business account that your team uses in WhatsAble.
- Send WhatsApp messages: text, approved templates, images, video, audio, documents, stickers and reply buttons.
- Read conversations, messages and their delivery status: sent, delivered, read or failed.
- Read the timeline of a conversation: what the customer, your team and the AI bot did, in order, and why the bot did it. See Read what happened in a conversation.
- Find, create and label contacts.
- Create templates and submit them to Meta for review.
- Schedule messages and launch broadcasts to many contacts.
- Configure AI bots, their knowledge documents, and the files they may send.
- Switch the AI off in one conversation, or stop its follow-ups to one contact while it keeps answering.
The same API serves the WhatsAble apps for Make, Zapier and n8n, and the WhatsAble MCP server. A flow that one of them can build, your code can build too.
Each operation has its own page in this section. A page shows the method and the URL, the scope the key needs, the parameters, the request body fields, every response, and a request to copy. Each page also has a Markdown copy: add .md to its URL.
Base URL and the OpenAPI file
Every route starts with https://api.whatsable.app/v1. The API serves its OpenAPI 3.1 file at https://api.whatsable.app/v1/openapi.json. It needs no key (the route's page). Use the file to generate a client.
Authentication
Send your API key in the Authorization header as a bearer token:
curl https://api.whatsable.app/v1/me \
-H 'Authorization: Bearer nfy_live_...'- Create a key in WhatsAble, under Settings > API keys. A key starts with
nfy_live_. - The API refuses a key in any other place with
401 api_key_in_wrong_place. - A key from the legacy API does not work here. The API answers
401 legacy_api_key_not_accepted. - Each key has scopes. The page of each route names the scope it needs, from the route's
x-required-scopein the OpenAPI file. A key without that scope gets403 insufficient_scope. - A key can be limited to some of your numbers. A request for another number gets
403 number_not_allowed_for_key. GET /v1/mereturns the organization and the key behind a request. Use it as a connection test.
Keep the key on your server. Never put it in a browser, a mobile app or a public repository.
Request and response format
- Bodies are JSON. Field names are
snake_case. - Every resource has an
objectfield, such as"message", and anid. - A field that does not apply is
null. The API never omits it. - Phone numbers are E.164 with a leading
+, such as+5491155550000. - Times are RFC 3339 in UTC, such as
2026-10-27T14:00:00Z.
Lists and pagination
A list route returns one page, newest first. The response has next_cursor. To get the next page, send that value as the cursor query parameter. On the last page, next_cursor is null. A cursor is valid for 24 hours.
Idempotency
Every POST that creates something accepts an Idempotency-Key header. The API remembers the key for 24 hours, per API key.
- A retry with the same key and the same body gets the stored response. The header
Idempotent-Replayed: truemarks it. The API does not do the work twice. - A retry with the same key and another body gets
409 idempotency_key_reused.
Send a new key for each new request, and the same key for each retry of it.
Values that can grow
An enum marked x-open-enum: true in the OpenAPI file can gain new values within v1. Your code must accept a value it does not know.
Errors
Every 4xx and 5xx response has the same body. Branch on error.code, which is stable. The title and message are for people, and their wording can change. API v1 errors lists every code.
{
"error": {
"type": "permission",
"code": "insufficient_scope",
"title": "...",
"message": "...",
"how_to_fix": "...",
"param": null,
"source": "whatsable",
"owner": "caller",
"retryable": false,
"docs_url": "...",
"details": { "required_scope": "messages:send" },
"request_id": "..."
}
}typefollows the HTTP status:invalid_request400,authentication401,permission403,not_found404,conflict409,precondition_failed412,unprocessable422,rate_limit429,internal500,unavailable503.ownersays who must act:caller(change the request or the account),whatsable, ormeta.retryablesays whether the same request can succeed later without a change.request_idrepeats theX-Request-Idheader of the response. Give it to support when you ask about a request.
Responses carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers. A 429 rate_limited response has Retry-After, in seconds.
A send that WhatsApp refuses still answers 201
A 201 Created from a send means that WhatsAble recorded the message. It does not mean that WhatsApp accepted it. Always read status in the body.
status: "sent": WhatsApp accepted the message. Delivery and read follow as status changes.status: "failed"witherror.retryable: true: WhatsApp refused the message for a reason that passes, such as a rate limit or an outage. Try again later with a newIdempotency-Key. The old key replays the failure.status: "failed"witherror.retryable: false: WhatsApp refused the message for good, or the result is unknown (timeout). Do not send it again. Showerror.how_to_fixto the person who can fix the cause.- A
4xxstatus: WhatsAble refused the request before it called WhatsApp. Change the request or the account.
Every type except template is a session message. To a new contact, or to a conversation whose 24-hour window is closed, it answers 422 session_window_closed. Send a template instead. See Send a message.
Read what happened in a conversation
GET /v1/conversations/{id}/timeline returns the history of one conversation in one list: the messages, the internal notes of your team, each decision of the AI bot with its reasoning, the changes of the AI status and of the bot, and the labels that went on or came off the contact. See Read a conversation's timeline.
It answers the questions that a list of messages cannot:
- Why did the bot hand this chat off to a person? The
ai_decisionentry hasoutcome: "handed_off", the reason code, and the bot's own words about why. - Who added the label "Lead", and when? The
label_addedentry names the bot, the teammate, the API key (named only when it is the key making the request) or the automation that added it. - Did the bot try to do something and fail? A decision lists every label the bot asked for and every file it asked to send, with what became of each one. Refused requests are included.
- When did a teammate take over, and what did the team note about the customer?
- Which follow-ups did the bot send after the customer went quiet, and which did it hold back?
Common uses:
- Review the bot's work. Read the decisions of a day and find the replies and handoffs to improve in the bot's instructions.
- Hand a chat over with context. Before a teammate or an AI assistant answers, it reads what the bot already said and why it stopped.
- Audit. Answer "who changed this, and when?" for labels, the AI switch and the bot of a conversation.
- Sync a CRM or a data warehouse. Poll with
created_afterand store each new entry.
How a page looks
The timeline is a list like every other list of the API: newest first, with limit, cursor and next_cursor. Filter with types, such as types=ai_decision,label_added.
{
"object": "timeline_entry",
"id": "dec_cm2v8d1a00001qz8h6n7o8p9q",
"type": "ai_decision",
"at": "2026-10-28T13:52:12Z",
"actor": { "type": "ai_bot", "id": "cm2v8b1a00001qz8h3j4k5l6m", "name": "Sales" },
"ai_decision": {
"trigger": "reply",
"outcome": "handed_off",
"reason_code": "price_or_terms",
"reasoning": "Asked for a discount, which only the team can give.",
"labels": [],
"files": [],
"message_id": "cm2v8m1a00003qz8h9x8w7v6u",
"attempt": null,
"decided_at": "2026-10-28T13:52:05Z"
}
}typenames the one payload key the entry carries:message,note,ai_decision,ai_status_changed,ai_bot_changed,label_addedorlabel_removed. New types can appear. Skip a type you do not know.actorsays who made the entry happen:customer,member(a teammate, with their name),api_key,ai_bot,automation(a rule or a scheduled job) orsystem(WhatsAble or WhatsApp).reasoningis the bot's own account of its decision. It is not a verified fact. Emails, phone numbers and WhatsApp user IDs in it are replaced with[redacted].- A page can be empty and still have
has_more: true. Follownext_cursoras usual.
Scopes
The route needs the messages:read scope. Two kinds of entries need one more scope:
| Entries | Extra scope |
|---|---|
message, note | none |
ai_decision, ai_status_changed, ai_bot_changed | ai:read |
label_added, label_removed | contacts:read |
Without the extra scope, a page leaves those entries out. A request that names them in types gets 403 insufficient_scope. The Full access, Send and read, and Read only presets hold all three scopes, so this matters only for a key with custom scopes. Use such a key when an integration may read the messages but must not see what the bot decided.
The route needs API access on your plan, and not the AI chatbot feature. The history of a bot stays readable after your plan stops including it.
How it fits with the rest of WhatsAble
- Labels. Labels belong to the contact, and a contact has one conversation per number. A label change shows in the conversation where it happened: the bot's change in the conversation the bot answered, a teammate's change in the chat they changed it in, a keyword rule or a broadcast on its number. A change made on the contact itself, such as one through
POST /v1/contacts/{id}/labels, shows in every conversation of the contact. - The team. A teammate's messages, notes and takeovers carry their name. The timeline is read with an API key, and only the owner and Admins create keys (API keys). Internal notes can be read through the API, but not written.
- The AI agent. The decisions are the same records that the inbox shows beside each bot reply (AI Chatbots). A reply, a handoff, a discarded reply and each follow-up attempt are decisions. A handoff also shows as an
ai_status_changedentry, and so does a teammate who pauses the bot. - The rest of the API.
GET /v1/conversations/{id}is the conversation's current state, such as its 24-hour window and its AI status.GET /v1/messages/{id}is one message and its delivery. The timeline is how the conversation got there. - The MCP server. An AI assistant connected to WhatsAble reads the timeline with the
read_conversationtool, as a transcript, oldest first. Ask it "what happened in this chat, and why?". The tool takesconversation_id,limit,cursorandtypes, but notcreated_after: polling is a job for the API. - Make, Zapier and n8n. The WhatsAble apps have no timeline module yet. Call the route with your API key from the HTTP module of Make, Webhooks by Zapier, or the HTTP Request node of n8n (Make, Zapier, n8n).
What it does not show
- A reply the bot is still writing, or one waiting to retry. It appears when it settles.
- AI decisions from before your account moved to the current WhatsAble platform.
- Where an older label change happened. A change made before the timeline launched, by anyone but the bot, shows in every conversation of the contact, because its record does not say where it was made.