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:
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 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:
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:
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 incoexistence_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
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:
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:
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
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 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
Creation requires
number_id, lower-case/underscore name, language, category (MARKETING, UTILITY, AUTHENTICATION) and Meta-format 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 to receive events without polling.