Capabilities: what Watobot supports for the WhatsApp API
What Watobot supports, so you can check it before you start: onboarding a number with coexistence or as API only, sending with limits that protect the number, message templates and moving them across, and leaving us again (de-registering and disconnecting). The same list is the one the project's README documents. Using it from an AI tool is a separate how-to.
What you can do: the API
Everything Watobot does for a number is available three ways: in the dashboard, as a call that takes your Watobot API key as Authorization: Bearer <key>, and as a tool in the MCP server (below). This page lists what is supported, so you can check it before you start, and the number's page in the dashboard shows each call with its address filled in. Answers are JSON with ok (and error when ok is false), except sends and the template list, create, edit and delete, which return Meta's response unchanged.
Numbers: /numbers
<id> is the phone number ID. Every call takes the key as the bearer token; the two reads also take a sign-in.
| Method and path | What it does | Body |
|---|---|---|
GET /numbers |
The account's numbers: where each is in onboarding (step), whether it is on the Business app, the syncs, usage, the sending status and the display name. Never the stored token (hasToken only). |
|
POST /numbers |
Sign-up: exchanges the Embedded Signup code for the Meta token, checks with Meta that the WABA and phone number really are the signing-up user's (and that no other Watobot account holds the number), stores the token encrypted under the key, and starts the wizard for the number. Signed in, with the key in the body. | code, wabaId, phoneNumberId, apiKey, event |
GET /numbers/<id> |
One number as above, plus its template-send limits, the stored sending status and nextRefreshAt. It answers from what is stored and never asks Meta, so it is safe to repeat. With ?refresh=true (the key, not a sign-in) it asks Meta right now: the number's status and display name approval, quality rating, messaging limit tier, account review and ownership, the apps subscribed with their callback URL override, the token's scopes and expiry, and Meta's own health_status (returned as meta), and stores the sending status (below). Meta is asked at most once in ten minutes: a refresh before then is 429 with Retry-After and the stored data. Each part of meta is a separate read, and a part Meta refuses says why. Never the token. 404 for a number that is not the caller's. |
|
GET /numbers/<id>/notifications |
What Meta has told us about the number, newest first, kept for a month: a ban or restriction, a display name decision, a messaging limit change, a template paused or reinstated, and what a refresh found different. ?limit= (up to 50) and ?before= (the nextBefore of the last page) page back through them. Never a message. |
|
PUT /numbers/<id>/webhook |
Checks the URL answers Meta's handshake, subscribes to it, and records it. During onboarding it also moves the wizard on. With noCallback instead of a URL it subscribes with no override: incoming events go to Watobot's own webhook, which discards them, and a coexistence number's contacts and history sync is skipped (Meta allows it once, to a URL). |
overrideCallbackUrl or noCallback |
PUT /numbers/<id>/display-name |
Asks Meta to review a new display name. Refused while an earlier change is still in review, so nobody resubmits while waiting. | displayName |
POST /numbers/<id>/register |
Registers an API-only number. Refused for a number on the WhatsApp Business app. Waits up to 45 s for Meta. | pin (6 digits) |
POST /numbers/<id>/deregister |
Takes an API-only number off the Cloud API. Refused for a number on the Business app (disconnect there). | |
POST /numbers/<id>/disconnect |
What leaving Watobot does on Meta's side: unsubscribes our app, and de-registers an API-only number if asked. | deregister (boolean) |
POST /numbers/<id>/messages |
Sends a message through Meta with the stored token. Template sends are rate limited: a refused one gets 429 with Retry-After, and every template send answers with `X-RateLimit-Remaining-<Minute |
Hour |
GET /numbers/<id>/limits |
Where the number stands against its template-send limits, per minute, hour and day window: accepted, attempted, previous window's count, limit, remaining and when it resets. Free-form messages are not limited. |
Templates: /templates, /templatesBulk
| Method and path | What it does | Body |
|---|---|---|
GET /templates/<id> |
Lists the account's templates (id, name, status, category, language, components, quality_score). It returns the first 100 only: there is no paging yet, so an account with more has the rest left out. The response is what /templatesBulk/<id>/import takes. |
|
POST /templates/<id> |
Creates a template. | Meta's template body |
POST /templates/<id>/<templateId> |
Edits a template. | Meta's template body |
DELETE /templates/<id>?name=<name> |
Deletes every template with that name. | |
POST /templatesBulk/<id>/migrate |
Copies approved templates from a previous Messaging account (Watobot must be a partner on it). Answers outcome: "unreadable" if we can't read it. |
{ sourceWabaId } |
POST /templatesBulk/<id>/import |
Creates templates from an exported list, up to 100 at a time. | { templates } |
The wizard: /onboarding
| Method and path | Credential | What it does | Body |
|---|---|---|---|
POST /onboarding/callback-check |
Signed in or key | Does Meta's handshake against a callback URL, before there is a number. | overrideCallbackUrl (and apiKey for a key) |
POST /onboarding/test-events |
Signed in or key | Posts one test webhook of each kind to a URL, with fresh codes. One send per 30 seconds. | overrideCallbackUrl |
POST /onboarding/test-codes |
Signed in or key | Says which of the four typed codes match the last send. | codes |
PUT /onboarding/<id>/step |
Key | Moves the wizard between the steps the user chooses or confirms. | step |
POST /onboarding/<id>/sync |
Key | Requests the contacts, then the history sync (each once). | syncType |
POST /onboarding/<id>/sync/skip |
Key | Skips the contacts and history sync for good (Meta allows each once). Only at the sync step. |
Sending status. What decides whether a send goes out is kept on the number as health: the issues that currently restrict it, grouped by what they are about (account, business, number: the three parts Meta reports on) and by what they are (payment, ban, review, connection, restriction, quality, cooldown, name (the display name is not approved yet), business, or code_<N> for a Meta error we have no wording for). An issue exists only while it applies; when it lifts it is deleted. Every write also stores each scope's status, the number's overall status and limits (the lowest ceiling per window), so the stored health is what the dashboard and API show. An issue with an end time is dropped by the next write after it ends, and the relay ignores it from then on, so the dashboard can show it for a few minutes longer until the next refresh. It is set by:
- Meta's notifications to our webhook (below), applied as they arrive. An event sets or clears only its own issue: a lifted ban leaves a payment problem where it is, and an event older than what is stored for that issue is ignored.
- A refresh (
GET /numbers/<id>?refresh=true, which is the dashboard's refresh button and an AI tool'sget_numberwithrefresh), at the end of onboarding, and a send Meta refuses. A refresh reads Meta's whole picture and replaces what the notifications had set, except anything that arrived while it ran. Meta is asked at most once in ten minutes.
Each issue says what it does to sending (send): limit, block or disable. A scope's status is the worst of its issues, and the number's is the worst of the scopes:
| Status | When | What the relay does |
|---|---|---|
disable |
The account is banned or scheduled to be disabled, deleted, rejected in review, or the number is disconnected | Refuses every send with 403, free-form messages too |
block |
Meta says the account or number is blocked (for example a payment method problem), a restriction on business-initiated messaging, or Meta's spam rate limit (a pause of an hour) | Template messages get 403, or 429 with Retry-After when the block has an end time. Free-form replies still go through. |
limit |
The number is flagged or of low quality, or the business is unverified | The issue's limits apply: at most 5 template messages a day for a flagged or low-quality number (our own cautious figure; Meta gives none), and always Meta's daily limit. An unverified business changes nothing by itself. |
ok |
No issue | Normal limits. |
An issue may carry limits per window; the lowest of these and Meta's daily messaging limit (250 while the business is unverified, stored as health.tier) is health.limits, which caps what the number has earned (see Rate limits). Each issue has a title, a message that says what is wrong and what to do, and where we know one a link, written for the cases we know and in Meta's words for any other. A refused send returns the blocking issues. The number's page shows one chip per scope (OK, Limited or Blocked) and the popup lists the issues. Any other account-level field Meta sends is kept as a notification that gates nothing. The wording for every signal is one table, functions/src/helpers/rules.ts.
What Watobot does not do
- It does not keep your messages, contacts or chat history. Incoming messages, and the one-time contacts and history sync, go straight from WhatsApp to your callback URL. Outgoing messages are counted, never stored.
- It does not do Meta's sign-up for you: the Facebook login and the code or QR in the WhatsApp Business app are always done by you, in the browser.
- It does not queue messages yet: a send over a limit is refused (429) and can be retried.
For AI tools: the MCP server
An AI tool can use the calls above through MCP: check a callback URL, send test events, get the link for Meta's sign-up, read a number's state, sync, register, send messages, manage templates, de-register and disconnect. deregister_number, disconnect_number and delete_template are marked destructive. The tools are one piece of code, offered two ways:
| Hosted | Local | |
|---|---|---|
| For | claude.ai, ChatGPT, and Claude Code or Cursor over http | Claude Code, Claude Desktop, Cursor and others that launch a program |
| Where it runs | our servers: POST /mcp |
on the user's computer: the `watobot-mcp` package, over stdio |
| The key | Authorization: Bearer <key>, or in the address (/mcp/<key>) for tools that only take an address |
the WATOBOT_API_KEY environment variable |
| Setup | the dashboard's API key page builds the address and commands from the key you paste | claude mcp add watobot -e WATOBOT_API_KEY=<key> -- npx -y watobot-mcp |
There is no OAuth: the Watobot API key is the credential, as everywhere else. The hosted server is stateless (each request is a new server, nothing about the caller is kept), checks the key against the account before answering anything (401 otherwise), and never logs it. A new user signs up on the site and creates the key there (it is made in the browser and shown once, so no tool can make it). Meta's sign-up (Facebook login, the code or QR in the WhatsApp Business app) is always done by the person: the start_onboarding_link tool gives a link that opens the dashboard wizard at that step, with the checks already done, and after the sign-up the wizard tells them to go back to their AI tool, which reads the result with get_number.