> ## Documentation Index
> Fetch the complete documentation index at: https://docs.60db.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# WhatsApp resources

> Manage workspace numbers, contacts, conversations, templates and usage

All paths are relative to `https://api.60db.ai`. Authenticate with a 60db API key with `whatsapp` scope. API keys are pinned to their registered workspace; passing a different workspace ID does not change their scope.

Responses use `{ "success": true, "data": ... }`. List endpoints support `limit` (1–100, default 50) and `offset` (0–100,000), returning `pagination: {limit, offset, has_more}`. Number lists and usage are not paginated.

## Numbers

`GET /whatsapp/numbers` returns your 60db number UUIDs, business display names, connection status, quality rating, `connection_error`, `provider_state`, `coexistence` and `coexistence_state`. Connection tokens are never returned. Confirmed invalid/expired access sets `action_required` with `META_TOKEN_REVOKED`; reconnect through Embedded Signup before continuing. Provider errors and temporary token-probe failures alone do not prove revocation.

After a definite authentication/permission failure, the backend checks the captured token. A valid token whose matching-app response confirms that `whatsapp_business_management` or `whatsapp_business_messaging` is no longer granted sets `action_required` with `META_PERMISSIONS_MISSING`. It uses the same stop transaction: credentials are cleared, unfinished AI work and campaigns stop, and history/accepted outcomes remain. Template/media calls return HTTP 409 with this code; queued sends retain their linked failure record. Missing/malformed probe data and failed probes preserve the connection. A probe for an old token cannot stop its replacement. Endpoint error codes alone do not prove missing App Review approval or removed assets.

When both permissions remain granted on a valid matching-app token, the backend checks the stored WABA's complete native phone list. Only a successful, well-formed exhausted list without the exact stored phone sets `action_required` with `META_ASSET_ACCESS_REMOVED`. It stops that connection through the same transaction; other stored numbers on the account remain connected. A phone found on a later page preserves access. Denied/unavailable reads, malformed phone IDs, malformed/repeated pagination and the 100-page verification ceiling preserve the connection and original sanitized failure. Fixed-resource opaque cursors are followed; provider `paging.next` URLs are never fetched. The read requests only phone IDs and does not filter the list. This establishes that the original account/phone binding is unavailable to the captured token; it does not claim whether Meta deleted, moved or unshared the asset.

Template/media requests, including binary downloads, return HTTP 409 with the confirmed asset-access reason. A send already attempted retains its linked provider failure rather than being automatically resent. Pending messages, AI generation and campaigns stop; history and accepted delivery outcomes remain. Captured-token comparison prevents an old probe from stopping a replacement or reclassifying a manual disconnect. Reconnect through Embedded Signup after restoring the required asset access; the assistant remains disabled/paused and cancelled campaigns do not restart.

App Review Advanced Access and business asset access can both cause Meta permission error `200`; a failed asset read does not distinguish them. In that case, preserve the original failure and review app approval/asset grants in Meta before retrying. The backend does not inspect token internals or infer asset access from omitted `granular_scopes.target_ids`.

`provider_state` records the latest supported `account_update`, `account_review_update`, `phone_number_name_update` and `phone_number_quality_update` signals, each with an `at` timestamp in epoch seconds. Account decisions are bound to the stored business account; phone decisions also require its exact stored display number. Approved name updates replace the verified name; rejected requests preserve the last approved name. Quality limit events do not imply a color: the backend reads the current native rating, or returns `quality_rating: null` when that read cannot be verified.

Terminal account events stop affected connected/prepared numbers as `action_required`, clear credentials and cancel pending work while retaining history and accepted delivery outcomes:

| Provider event | Connection error |
| - | - |
| `DISABLED_UPDATE` with `DISABLE` | `META_ACCOUNT_DISABLED` |
| `ACCOUNT_DELETED` | `META_ACCOUNT_DELETED` |
| `ACCOUNT_OFFBOARDED` | `META_ACCOUNT_OFFBOARDED` |
| Verified `PARTNER_APP_UNINSTALLED` | `META_APP_UNINSTALLED` |
| Verified `PARTNER_REMOVED` | `META_PARTNER_REMOVED` |

Partner events bind the customer account through `waba_info.waba_id`. App install/uninstall requires a matching configured app ID. Partner-added/removed requires the receiving business portfolio to match backend `META_BUSINESS_ID`; missing configuration or wrong/unknown account/app/portfolio bindings are ignored. If the portfolio ID is configured, it also binds app install/uninstall. This ID belongs to the provider's Meta portfolio, not the customer WABA, and remains backend configuration.

Older signals are ignored; terminal access loss takes priority over a non-terminal account decision with the same second-resolution timestamp. A stop received during the current signup remains applicable if processed after activation. Later supported notices in the same receipt still persist after stopping. `SCHEDULE_FOR_DISABLE`, reinstatement, review approval, partner-added/app-installed and `ACCOUNT_RECONNECTED` notices record state without reconnecting a number or resuming the assistant. A new signup preparation resets the preceding connection's provider state, retaining account restriction/violation records when reconnecting the same WABA. See [webhook events](https://docs.60db.ai/api-reference/whatsapp/webhook-events.md) for `number.updated` subscriptions. Unknown variants remain ignored; coexistence/history synchronization has separate contracts.

### Policy restrictions

`ACCOUNT_VIOLATION` records a bounded `violation_type` in `provider_state.account_violation`; a warning alone does not stop the connection. `ACCOUNT_RESTRICTION` records supported restriction types in `provider_state.account_restriction.restriction_info`, with `expiration` (epoch seconds), per-type `at` and bounded optional `remediation`. Account notices cannot overwrite these independent records. Omitted types are retained until their recorded expiry; older per-type updates cannot release a newer restriction. Same-second conflicts retain the longer or unbounded restriction. Missing/zero expiry is conservatively treated as unbounded until a later explicit update. Unknown types and malformed payloads are ignored.

60db checks affected actions at queueing and again before provider dispatch:

| Restriction | Local behavior |
| - | - |
| `RESTRICTED_BIZ_INITIATED_MESSAGING` | Blocks template sends and broadcast creation/scheduling; free-form customer replies remain available |
| `RESTRICTED_CUSTOMER_INITIATED_MESSAGING` | Blocks free-form replies and template replies in an open customer reply window; inbound history remains available |
| `RESTRICTED_DIRECT_SEND_UTILITY_TEMPLATES` | Blocks utility template sends, using the synced approved category |
| `RESTRICTED_UTILITY_TEMPLATES` | Blocks utility template creation; does not by itself block existing approved templates |
| Phone-add and calling restrictions | Retains metadata; this module does not create Meta phone assets or offer calls |

Blocked requests return `META_ACCOUNT_RESTRICTED` (409). These guards express 60db's mapping to its existing template/reply operations; Meta remains the authority on account enforcement. The number stays connected and credentials are retained. New inbound messages do not start AI generation while replies are restricted. An existing run rechecks before/after each inference stage, so a newly restricted result is discarded without a new credit charge. Already completed stages remain billed. Dispatch waits and restriction recording serialize on the number: an accepted call already in progress retains its outcome; subsequent restricted calls do not start.

Expiry permits new eligible actions; failed messages and broadcast recipients are not automatically resent. Restrictions do not reconnect a stopped number, restart campaigns or resume disabled/paused AI. Same-account reconnect retains restrictions and cannot bypass their guards. Review enforcement/remediation in Meta Business Support Home or WhatsApp Manager. This implementation does not infer app-review eligibility or arbitrary asset removal from a provider error alone.

`GET /whatsapp/config` returns public connection readiness, app/config IDs, Graph API version and workspace plan state. It does not return the app secret or encryption key.

Connect in the dashboard using Meta Embedded Signup. The setup endpoints require a logged-in owner/admin; API keys cannot connect or disconnect numbers:

| Method | Path | Body |
| - | - | - |
| POST | `/whatsapp/signup/sessions` | None; returns session `id` and expiry |
| POST | `/whatsapp/signup/exchange` | `session_id`, Meta authorization `code` |
| POST | `/whatsapp/signup/assets` | Authorized `session_id`, `waba_id`; lists eligible Business app phone IDs/display names |
| POST | `/whatsapp/signup/complete` | `session_id`, `waba_id`, `phone_number_id`, six-digit `pin`; `coexistence` defaults to false |
| POST | `/whatsapp/numbers/{id}/disconnect` | None |

Exchange the code immediately: Meta's code expires after 30 seconds. Wait for both successful exchange and the separate Embedded Signup finish event before completing. The backend binds the session to the initiating user/workspace and validates the exact selected number against the selected business account, including subsequent number-list pages. An invalid connection token, missing permissions or an incomplete provider verification fails setup; it never falls back to another accessible number.

If the exchange response is lost after authorization was recorded, retry `/whatsapp/signup/exchange` with the same session and code. The backend returns the recorded current state without exchanging the single-use code again: `authorized`, `processing` when number setup has begun, or `complete`. `SIGNUP_EXCHANGE_IN_PROGRESS` (409) means authorization is still being recorded; retry the same request. `SIGNUP_CODE_CHANGED` (409) rejects a different code for the claimed session. `SIGNUP_AUTHORIZATION_RESTART` (409) means authorization was not recorded and the attempt failed: start a new session and reopen Meta for a fresh code. An expired unfinished or failed session requires a new signup; a recorded completed session remains recoverable. Only the code's digest is retained, and responses contain no account token. This does not recover an upstream exchange whose result was never recorded locally.

If the completion response is lost, retry `/whatsapp/signup/complete` with the same session, account, number and coexistence mode. An already recorded completion returns HTTP 200 with that number's **current** state, without another Meta subscription/registration request or connected event. It cannot reactivate a subsequently disconnected number. The PIN is not stored; changing it on a completed retry does not change the Meta PIN. Changing the selected assets returns `SIGNUP_PAYLOAD_CHANGED` (409). Recovery remains bound to the initiating user and workspace.

`CONNECTION_SETUP_UNCONFIRMED` (503) means an external setup outcome or local progress write could not be confirmed. Retry the same session and selected assets before session expiry. The backend retains the exact encrypted preparation and checks token/asset access, this configured app's WABA subscription and, after an uncertain ordinary registration, the selected phone's `CONNECTED` status. Another app's subscription or another phone never satisfies recovery. Unavailable reads, rate limits and an unconfirmed subscription/status keep the attempt recoverable; they do not announce success. An uncertain subscription or registration POST is never blindly repeated. A step not yet dispatched can run after preceding state is verified. Successful recovery returns HTTP 201 and records one connection event. Status verification establishes current API readiness; it does not prove that the interrupted request changed the PIN. PINs are never stored, and retrying an already attempted registration cannot change the PIN. Definitive setup rejection fails the session and clears its tokens.

`CONNECTION_SETUP_IN_PROGRESS` (409) means another setup currently holds that number's registration lock; retry the same session after it finishes, within session expiry. This response does not consume a competing authorized session. A stopped/replaced preparation returns `CONNECTION_SETUP_CHANGED` (409); an expired unfinished session returns `SIGNUP_SESSION_EXPIRED` (409). Both require a new session. Failed preparation clears its stored tokens; abandoned processing gets at most five minutes of grace beyond session expiry for cleanup or a request already in progress. New recovery requests cannot begin after expiry. Expired processing is never proof of a connected number. Recovery checkpoints are private backend state and require `add_whatsapp_setup_checkpoints.sql` after the preceding migrations.

Disconnect stops local receipt and pending dispatch while retaining conversation history. It clears the stored connection token, disables the assistant, pauses existing conversations and cancels pending assistant runs and campaigns. It waits for provider dispatch already in progress; accepted sends retain their outcomes and interrupted claimed sends remain unconfirmed. It does not delete the Meta account or unsubscribe other numbers sharing that account. Owners/admins can disconnect an existing number even after paid access ends; connecting still requires an active paid platform plan.

Reconnect supplies a new encrypted token and clears the connection error after setup succeeds. A late failure from the old token cannot disable that replacement. The assistant stays disabled/paused after stopping a number; explicitly configure and resume it when ready. Delivery/read callbacks for already accepted messages continue updating history after stopping the number; new inbound messages are ignored while stopped.

Reconnect also stops pending work from the preceding connection before preparing the new token. History and accepted sends remain intact. During setup, `connection_error` is `CONNECTION_SETUP_PENDING`; interrupted attempts may show `CONNECTION_SETUP_UNCONFIRMED`, while failed/abandoned preparations may show `CONNECTION_SETUP_FAILED` or `CONNECTION_SETUP_EXPIRED`. A successful reconnect keeps the same 60db number UUID and does not restart cancelled campaigns or resume the assistant automatically.

### Business app callback support

`coexistence: true` on completion skips ordinary registration only after the backend verifies the exact phone's native boolean `is_on_biz_app: true` and `platform_type: "CLOUD_API"`. A negative result returns `META_COEXISTENCE_UNCONFIRMED` (400); malformed data cannot confirm mode. The PIN is unnecessary in this verified mode. Unconfirmed mode does not replace an existing working number. Same-attempt recovery rechecks it. Replacing a token or disconnecting locally does not establish new provider onboarding: same-account/mode reconnect preserves sync outcomes/sharing state/history. Verified provider offboarding records durable `coexistence_state.offboarded_at`; a later native signup can then create a fresh sync epoch. Manual disconnect cannot erase this proof.

After authorization exchange, `POST /whatsapp/signup/assets` with `session_id` and the finish event's `waba_id` returns an array of `{phone_number_id, display_phone_number, verified_name}` for eligible dual-mode phones. It requires the initiating user/workspace's authorized unexpired session, matching Meta app and both grants. Paging uses the fixed WABA resource, never returned URLs; duplicate IDs are removed. Tokens/arbitrary provider fields are omitted. Select the intended phone explicitly, then complete with it; discovery never registers or silently chooses a different phone. Completion still verifies exact ownership and mode.

For a connected verified mode, signed `history`, `smb_app_state_sync` and `smb_message_echoes` callbacks bind to the stored account, phone/display number and current connection attempt. Historical messages use `source: "whatsapp_history"` and provider timestamps for chronology. They never increment live unread counts, open a customer reply window, become the assistant's live customer turn or emit `message.received`. Media callbacks hydrate the original history message by provider ID, including when media arrives first; they cannot create a guessed live conversation. Missing original assets remain placeholders.

`coexistence_state.history` records `receiving`/`complete` with nondecreasing `progress`, or `declined` with provider code `2593109`. An accepted provider initiation is not itself history consent. Address-book changes expose contact `app_state: {action, at}`; adds/edits update the name, removals retain a tombstone. Older updates are ignored and same-second removal wins. These changes never infer opt-in/opt-out, erase history/tags or create inbox conversations.

Business app sends use `source: "whatsapp_business_app"`, outbound direction and `message.mirrored`. They pause the conversation and cancel pending AI as a human handoff, without a second provider send, unread increment or reply-window extension. Deferred edits/media/revocations bind to the original number/contact even if the change arrives first. Edits set `app_updated_at`; revocations set `app_revoked: true`, clear the displayed original content and use `type: "revoked"`. Later edits/imports cannot restore a revoked message. Subscribe to `message.updated` for those changes and `number.updated` with `field: "history"` for import progress/decline.

### One-time Business app synchronization

Successful native completion atomically queues contact/history initialization in `coexistence_state.sync`. The existing worker rechecks live owner/admin `number:connect` permission and initiates contacts before history using `smb_app_data`. Accepted steps retain a support `request_id`; request acceptance does not mean the business shared its history. The separate `coexistence_state.history` records actual received progress/decline.

Each `sync.contacts`/`sync.history` step has a `status`: `pending`, `dispatching`, `accepted`, `unconfirmed`, `failed`, `blocked` or `expired`, plus a support ID/error when available. Dispatch intent is saved before the external call. A lost/malformed response, interrupted worker or failed acceptance checkpoint remains unconfirmed and is never blindly repeated. Failed/unconfirmed contacts block dependent history. Authorization outages retain pending undispatched work; denied actions become blocked. Explicit authorized reconnect can retry blocked steps that never dispatched, within the original window. It preserves accepted/failed/unconfirmed requests. This synchronization does not send customer messages or charge AI credits.

`sync.started_at` conservatively uses the initiating connection session's creation time for the 24-hour deadline; requests past it are expired without dispatch. Sync state also records the initiating `actor_user_id`. Stop waits for an already-started call and retains its accepted result; later calls do not start after disconnect. Shared access-loss verification runs after dispatch locks are released. Subscribe to `number.updated` with `field: "sync"` for these transitions. Unconfirmed/expired initiation may require checking Meta support and offboarding/re-onboarding; do not repeat an uncertain once-only request manually.

The Business app `PARTNER_REMOVED` callback without `waba_info` stops only the exact unique stored coexistent phone on the entry's customer WABA, and persists `offboarded_at` proof. It requires no `META_BUSINESS_ID`. Ordinary, wrong/missing/ambiguous phone/account and malformed portfolio variants are ignored. Existing portfolio/app-bound variants retain their original checks. Stop preserves history and pauses AI/campaigns through the shared transaction.

Backend verification, discovery, sync dispatch, callback intake and bounded offboarding are implemented locally. The dashboard accepts the configured Business app finish, explicitly selects a verified phone, shows request/import states and labels mirrored edits/deletions. Meta requires eligible/configured Embedded Signup and the three extra webhook fields. External configuration and real callback/request behavior were not verified through local fixtures; no real sync or deployment occurred. [Meta Business app onboarding](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users)

## Contacts

`GET /whatsapp/contacts?number_id={number_uuid}` lists contacts for a number. Omit `number_id` to list the workspace's contacts.

Optional filters combine with the number/workspace scope and pagination:

| Parameter | Meaning |
| - | - |
| `search` | Name substring (case-insensitive) or phone substring; maximum 200 characters |
| `tag` | Exact tag match; maximum 50 characters |
| `consent` | `opted_in`, `opted_out` or `not_recorded` |

`opted_in` requires a recorded opt-in with no later/current opt-out. Use it when preparing broadcast audiences; prior inbound history alone is not broadcast consent. For example: `GET /whatsapp/contacts?number_id={number_uuid}&consent=opted_in&tag=customer&limit=100`.

`POST /whatsapp/contacts` creates or updates a contact by business number and recipient phone:

```json theme={null}
{
  "number_id": "11111111-1111-4111-8111-111111111111",
  "wa_id": "919000000001",
  "name": "Customer name",
  "tags": ["customer"],
  "opted_in": true
}
```

Set `opted_in` only after explicit consent. `false` records an opt-out; omission preserves current consent. Name defaults to empty and tags default to an empty list, so include them when preserving an existing contact's fields.

## Conversations

| Method | Path | Options |
| - | - | - |
| GET | `/whatsapp/conversations` | Optional `number_id`, `status` (`open`/`resolved`) and `search` |
| GET | `/whatsapp/conversations/{id}` | Conversation and recipient details |
| GET | `/whatsapp/conversations/{id}/messages` | Messages, newest first; pagination supported |
| PATCH | `/whatsapp/conversations/{id}` | Any of `status`, `mode` (`manual`/`ai`), `assistant_paused`, `assigned_to`, `mark_read` |
| GET | `/whatsapp/messages/{id}` | One message and its delivery state |

Taking over sets `mode` to `manual`, pauses the assistant, and cancels pending generation, drafts and queued assistant messages. Sending an ordinary console/API message also takes over. See [assistant configuration and review](https://docs.60db.ai/api-reference/whatsapp/assistant.md) for resumption and native AI billing. Assignment accepts only a current member of the same workspace; `assigned_to: null` unassigns. Reading a conversation does not mark it read automatically. Send `mark_read: true` to reset its unread count.

## Templates

| Method | Path | Options |
| - | - | - |
| GET | `/whatsapp/templates` | Optional `number_id`; pagination supported |
| POST | `/whatsapp/numbers/{id}/templates/sync` | Refresh templates from Meta |
| POST | `/whatsapp/templates` | Create a template for review |

Creation requires `number_id`, lower-case/underscore `name`, `language`, `category` (`MARKETING`, `UTILITY`, `AUTHENTICATION`) and Meta-format `components`:

```json theme={null}
{
  "number_id": "11111111-1111-4111-8111-111111111111",
  "name": "support_followup",
  "language": "en_US",
  "category": "UTILITY",
  "components": [{"type": "BODY", "text": "Your support request has been updated."}]
}
```

Meta approval is required before sending. Parameterised or media templates need the corresponding Meta examples and components.

## Usage and events

`GET /whatsapp/usage` returns current calendar month message counts, failed/unconfirmed counts, connected numbers and billing attribution.

`GET /whatsapp/events` returns workspace events with pagination. These are workspace-normalised events; raw provider receipts are internal.

Use [webhook subscriptions](https://docs.60db.ai/api-reference/whatsapp/webhook-events.md) to receive events without polling.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.