# InboxWA API reference > The InboxWA API: the same surface the web app runs on, plus a reporting API for unread, unanswered, seen-by, receipts, replies and first-response times. Authenticate with a bearer API key. Base URL: https://inboxwa.online OpenAPI 3.0 document: https://inboxwa.online/openapi.json Human-readable page: https://inboxwa.online/docs/api Index for AI assistants: https://inboxwa.online/llms.txt Each guide and each call is also available as its own Markdown page. The links are in the section headings' descriptions and in https://inboxwa.online/llms.txt. ## Guides ### Overview (Also at https://inboxwa.online/docs/api/guide/overview.md) The InboxWA API is the same surface the web app runs on. Whatever your team can see or do in InboxWA, a key can see or do, under the same permissions. Only the calls documented in this reference are supported for integrations; see "What is supported" below. Requests and responses are JSON over HTTPS. The base address is `https://inboxwa.online`. Send phone numbers with the country code, digits only; a leading + and spaces are accepted, so `919876543210` and `+91 98765 43210` are the same number. A 10-digit number with no country code is taken to be an Indian number and has 91 put in front of it, so always include the country code for any other country. Timestamps in the Reporting API and in webhooks are ISO 8601 in UTC. - **The whole product.** Chats, contacts, leads, messaging, templates, automation, the AI agent, credits, team, SLA, integrations and more are reachable with a key. - **Messaging and contacts.** Send a message or a broadcast, and create and update contacts, their tags, attributes and consent. These are part of the core API, included in Growth and Scale. Reading contacts and chats with a key is part of the full API (Scale plan). - **Plans.** Starter has no API and no webhooks. Growth has the core API and five webhook events. Scale has every call, the Reporting API and all webhook events. See [Plans and API access](https://inboxwa.online/docs/api/guide/plans.md). - **The reporting API.** Purpose-built calls for unread, unanswered, seen-by, receipts, replies and first-response times under `/api/reporting/v1`. Requires the Scale plan. - **Push as well as pull.** Signed webhooks, and an MCP endpoint for your own AI tools. > **What is supported** The calls documented in this reference are the supported API. They keep their shape, and any change to one is listed in the changelog. Every other route accepts the same key but is the web app’s internal interface: it is not documented, it is unsupported for integrations, and it can change or disappear without notice. `GET /api/docs/routes` lists those routes for reference only. Do not build an integration on them. > **For AI assistants and other tools** This documentation is also published without JavaScript: the whole reference as [Markdown](https://inboxwa.online/docs/api.md), the [OpenAPI document](https://inboxwa.online/openapi.json), and an index for AI assistants at [/llms.txt](https://inboxwa.online/llms.txt). Every guide and every call has its own Markdown page too, for example [API keys](https://inboxwa.online/docs/api/guide/keys.md). ### Plans and API access (Also at https://inboxwa.online/docs/api/guide/plans.md) What the API includes depends on the plan. A call outside the plan is refused with a clear error, whatever scopes the key has. | | Starter | Growth | Scale | | --- | --- | --- | --- | | API keys | No | Yes | Yes | | What a key can call | Nothing | The core API | Every call, including reading contacts and chats and the Reporting API | | Outbound webhooks | No | 5 core events | All 11 events | | Zapier and Make | No | Yes | Yes | #### The core API (Growth and Scale) The calls an integration needs: send messages, keep contacts up to date, list templates, and connect Zapier or Make. - `POST /api/send-message`: Send a message - `POST /api/bulk-send`: Send a broadcast - `GET /api/templates`: List message templates - `POST /api/contacts`: Create or update a contact - `PUT /api/contacts/:phone/name`: Set a contact’s name - `PUT /api/contacts/:phone/status`: Set a contact’s status - `PUT /api/contacts/:phone/email`: Set a contact’s email - `PUT /api/contacts/:phone/company`: Set a contact’s company - `PUT /api/contacts/:phone/tags`: Set a contact’s tags - `PUT /api/contacts/:phone/notes`: Set a contact’s notes - `PUT /api/contacts/:phone/attributes`: Merge custom attributes - `DELETE /api/contacts/:phone/attributes/:key`: Delete a custom attribute - `POST /api/contacts/:phone/opt-out`: Mark a contact opted out - `POST /api/contacts/:phone/opt-in`: Re-subscribe a contact - `/api/zapier/*` (any method): Zapier triggers and actions - `GET /api/reporting/v1/whoami`: Check a key #### The full API (Scale) Every other call a key can make: reading contacts and chats, the Reporting API, deleting contacts, marking a chat read, team, SLA and credits, key and webhook management with a key, and the rest of the app’s routes. Only the calls documented in this reference are supported; see the Overview. #### Webhook events by plan - **Growth and Scale:** `message_received`, `message_sent`, `contact_created`, `handoff`, `tag_added`. - **Scale adds:** `message_status`, `conversation_seen`, `conversation_assigned`, `conversation_resolved`, `sla_breached`, `credits_threshold`. - **Starter:** none. A webhook set up while on a higher plan stops receiving events when the plan drops. #### When a call is outside the plan | Status | error | When | | --- | --- | --- | | 403 | `api_not_in_plan` | A Starter workspace used a key, or tried to create one. | | 403 | `api_access_not_enabled` | A Growth key made a call that is part of the full API. | | 403 | `webhooks_not_in_plan` | A Starter workspace tried to set up webhooks. | | 403 | `event_requires_scale` | A Growth workspace subscribed to an event that is part of Scale. | `GET /api/reporting/v1/whoami` shows what your plan includes: `api_tier` is `core` on Growth and `full` on Scale. ### Quickstart (Also at https://inboxwa.online/docs/api/guide/quickstart.md) Three steps from nothing to a sent message. The API is included in the Growth and Scale plans. - **Create a key.** Open **API Keys** in the sidebar and choose Create key. To send messages the key needs the `whatsapp_api` scope (or full access), and the workspace must have the WhatsApp Cloud API connected. For reporting, choose the read-only scopes. Copy the key now: it is shown once. [How keys work](https://inboxwa.online/docs/api/guide/keys.md). - **Check it.** `GET /api/reporting/v1/whoami` returns the workspace, the scopes and what your plan includes (`api_tier`). It is part of the core API, so it works on Growth and Scale, and it tells you why another call is refused. - **Send a message.** `POST /api/send-message` with a template, or with text inside the 24-hour window. It needs the `whatsapp_api` scope. **Check the key, then send a message** ##### curl ```bash # 1. Check the key curl https://inboxwa.online/api/reporting/v1/whoami \ -H "Authorization: Bearer iwk_yourkeyhere" # 2. Send a template message curl -X POST https://inboxwa.online/api/send-message \ -H "Authorization: Bearer iwk_yourkeyhere" \ -H "Content-Type: application/json" \ -d '{"to":"919876543210","type":"template","template":{"name":"order_confirmation","language":"en_US","parameters":["ORD-4271","2026-10-05"]}}' ``` ##### Node.js ```javascript const headers = { Authorization: "Bearer " + process.env.INBOXWA_KEY, "Content-Type": "application/json", }; const who = await fetch("https://inboxwa.online/api/reporting/v1/whoami", { headers }); console.log(await who.json()); const res = await fetch("https://inboxwa.online/api/send-message", { method: "POST", headers, body: JSON.stringify({ to: "919876543210", type: "template", template: { name: "order_confirmation", language: "en_US", parameters: ["ORD-4271", "2026-10-05"] }, }), }); const out = await res.json(); if (!res.ok) throw new Error(out.error); console.log(out.messageId); ``` ##### Python ```python import os, requests headers = {"Authorization": "Bearer " + os.environ["INBOXWA_KEY"]} print(requests.get("https://inboxwa.online/api/reporting/v1/whoami", headers=headers, timeout=30).json()) r = requests.post( "https://inboxwa.online/api/send-message", headers=headers, json={ "to": "919876543210", "type": "template", "template": {"name": "order_confirmation", "language": "en_US", "parameters": ["ORD-4271", "2026-10-05"]}, }, timeout=30, ) r.raise_for_status() print(r.json()["messageId"]) ``` ##### PHP ```php function inboxwa(string $method, string $path, ?array $body = null): array { $ch = curl_init("https://inboxwa.online" . $path); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("INBOXWA_KEY"), "Content-Type: application/json"], CURLOPT_POSTFIELDS => $body ? json_encode($body) : null, ]); return json_decode(curl_exec($ch), true); } print_r(inboxwa("GET", "/api/reporting/v1/whoami")); $out = inboxwa("POST", "/api/send-message", [ "to" => "919876543210", "type" => "template", "template" => ["name" => "order_confirmation", "language" => "en_US", "parameters" => ["ORD-4271", "2026-10-05"]], ]); echo $out["messageId"]; ``` #### Then ask a real question On the Scale plan, list the chats where a customer has been waiting more than 30 minutes. **Unanswered chats** ##### curl ```bash curl "https://inboxwa.online/api/reporting/v1/conversations/unanswered?older_than=30m" \ -H "Authorization: Bearer iwk_yourkeyhere" ``` ##### Node.js ```javascript const res = await fetch( "https://inboxwa.online/api/reporting/v1/conversations/unanswered?older_than=30m", { headers: { Authorization: "Bearer " + process.env.INBOXWA_KEY } }, ); if (!res.ok) throw new Error((await res.json()).message); const { data } = await res.json(); for (const chat of data) { console.log(chat.phone, chat.waiting_seconds + "s", chat.assigned_to?.name ?? "unassigned"); } ``` ##### Python ```python import os, requests r = requests.get( "https://inboxwa.online/api/reporting/v1/conversations/unanswered", params={"older_than": "30m"}, headers={"Authorization": "Bearer " + os.environ["INBOXWA_KEY"]}, timeout=30, ) r.raise_for_status() for chat in r.json()["data"]: print(chat["phone"], chat["waiting_seconds"], (chat["assigned_to"] or {}).get("name")) ``` ##### PHP ```php $ch = curl_init("https://inboxwa.online/api/reporting/v1/conversations/unanswered?older_than=30m"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("INBOXWA_KEY")], ]); $body = json_decode(curl_exec($ch), true); foreach ($body["data"] as $chat) { echo $chat["phone"], " ", $chat["waiting_seconds"], "s\n"; } ``` ### Authentication (Also at https://inboxwa.online/docs/api/guide/authentication.md) Send the key as a bearer token on every request: `Authorization: Bearer iwk_...`. Keys are created by a workspace admin on the **API Keys** page in the sidebar. The plaintext is shown once and only its hash is stored, so a lost key is revoked and replaced, never recovered. - **Expiry.** A key can be given an expiry of up to 730 days. An expired key is refused with `key_expired`. - **Rotation.** Rotate a key to get a replacement with the same scopes and limit. A grace period of up to 168 hours keeps the old key working while you switch over. - **Audit.** Every call, including refused and rate-limited ones, is logged per key with method, path, status and IP, and can be read for 30 days. - **Plan.** A workspace on Starter has no API: a key is refused with `api_not_in_plan`, and a key cannot be created. On Growth a key may make the core calls only; see [Plans and API access](https://inboxwa.online/docs/api/guide/plans.md). - **Bad credentials.** A request with no credential at all is answered 401. A key that does not exist or has been revoked is answered 403, and so is an expired one (`key_expired`). A 403 is not a reason to retry. - **Full-access keys are powerful.** A full-access key created by an admin can do anything that admin can, including creating, rotating and revoking keys. Give integrations scoped keys. > **Keep keys server-side** A key carries the workspace’s data. Do not embed one in a browser page or a mobile app. Use a read-only key for dashboards and give each integration its own key so one can be revoked alone. ### API keys (Also at https://inboxwa.online/docs/api/guide/keys.md) Every call is made with an API key, and keys are managed on the **API Keys** page in the sidebar. A workspace admin can create, edit, rotate and revoke keys there. Other members with the settings permission can see the list and each key’s activity. The same key-management calls are in the reference below; they need an admin, either a dashboard session or a full-access key created by an admin. A scoped key is refused with `scope_denied`. #### Create a key - **Name it** after the integration, for example "Order sync". The name shows in the key list and in the activity log. - **Choose the access.** Full access carries everything its creator can do. A read-only key can only read. A custom key holds exactly the scopes you choose; see [Scopes and permissions](https://inboxwa.online/docs/api/guide/scopes.md). - **Set an expiry** if you want one, from 1 to 730 days. An expired key is refused with `key_expired`. - **Set a rate limit** if you want one, from 10 to 100,000 requests per minute. Leave it blank for the default; see [Rate limits](https://inboxwa.online/docs/api/guide/rate-limits.md). - **Copy the key now.** It starts with `iwk_` and is shown once. Only a SHA-256 hash is stored, so a lost key is replaced, never recovered. #### Manage a key - **Edit** to rename a key or change its expiry or limit. Scopes are fixed when a key is created; to change them, create a new key. - **Rotate** to get a replacement with the same name, scopes and limit. A grace period of up to 168 hours keeps the old key working while you switch over, then it stops on its own. - **Revoke** to stop a key at once. This cannot be undone. Create and deploy the replacement first so nothing goes down. - **Activity** shows each key’s calls for the last 7 or 30 days: method, path, status and IP, including refused and rate-limited calls. The log is kept for 30 days. - A workspace can hold up to 20 active keys. #### Good practice - One key per integration, so any one can be revoked alone. - Scoped keys for integrations. A full-access key can also create and revoke keys, so if one leaks, so can everything it minted. - Read-only keys for dashboards and reports. - An expiry on keys you hand to a contractor or a trial. - Keep keys on your server, in environment variables or a secrets manager. Never put one in a web page, a mobile app or a git repository. - Rotate when someone who knew a key leaves, and check the last-used time on the list: a key nobody uses should not exist. > **Which plan does what** Keys are part of Growth and Scale; Starter has none. On Growth a key can make the core calls: sending, creating and updating contacts, listing templates and Zapier. Any other call is refused with `api_access_not_enabled`, whatever the key’s scopes. On Scale a key can call everything. `GET /api/reporting/v1/whoami` shows which applies to yours. See [Plans and API access](https://inboxwa.online/docs/api/guide/plans.md). ### Scopes and permissions (Also at https://inboxwa.online/docs/api/guide/scopes.md) A key carries `all`, or any combination of the scopes below. Scopes choose how much of the workspace the key reaches, from one screen to everything. A key can never do more than the admin who created it. Its effective access is the creator’s permissions intersected with its scopes. #### Read-only scopes A `:read` scope lets a key call GET endpoints in that area and nothing else. A key made only of read scopes is a read-only key: every write method is refused with `read_only_key`, on every route, including routes that carry no permission check of their own. Use these for dashboards and reports. | Scope | Read-only variant | What it covers | | --- | --- | --- | | `inbox` | `inbox:read` | Read chats and messages; claim, release, transfer, snooze, label and note conversations. | | `contacts` | `contacts:read` | Contacts, leads and deals, tags, attributes, segments, import. | | `campaigns` | `campaigns:read` | Bulk send, scheduled sends, campaign control. | | `automations` | `automations:read` | Workflows, auto-replies, drip sequences and their analytics. | | `templates` | `templates:read` | Message templates and WhatsApp Flows. | | `analytics` | `analytics:read` | Dashboards, engagement and message statistics. | | `whatsapp_api` | | Send through the Meta Cloud API. Layered with templates and campaigns. | | `contacts_delete` | | Hard delete and bulk delete of contacts. | | `contacts_export` | | Bulk export of contacts. | | `campaign_approve` | | Send directly and approve or reject pending bulk sends. | | `templates_delete` | | Delete WhatsApp templates. | | `inbox_assigned_only` | | Restricts inbox reads and sends to chats assigned to the key creator. | | `ai_agent` | `ai_agent:read` | Bot configuration: knowledge base, pricing, behaviour, on and off. | | `ai_kb_edit` | | Change the FAQs the bot answers from. | | `ai_pricing_edit` | | Change the pricing sheets the bot quotes from. | | `ai_behavior_edit` | | Change persona, system prompt and response style. | | `ai_audit_view` | | Read AI usage, cost and handover audit trail. | | `ai_provision` | | Turn the bot on or off for the workspace. | | `team_management` | `team_management:read` | Roster, invites, roles, performance, goals, SLA, audit log. | | `team_remove_member` | | Remove members from the workspace. | | `audit_view` | | Read the team audit log. | | `settings` | `settings:read` | Workspace settings, webhooks, integrations, API keys (list). | | `billing_view` | | Plan, usage, credits and invoices. | | `billing_manage` | | Change plan, payment methods and top up credits. | > **Reporting API access** The reporting calls accept `inbox` or `contacts` (and their `:read` variants) for chat content, and `inbox`, `team_management` or `analytics` (and `:read`) for team accountability data such as who opened a chat and how fast replies were. ### Rate limits (Also at https://inboxwa.online/docs/api/guide/rate-limits.md) Each key is limited per minute. The default is 600 requests per minute per key, and a key can be given its own limit from 10 to 100,000. Some long-standing integrations are exempt. > **Sending is not throttled by the default** Sending messages is metered by your credits and by WhatsApp’s own limits, not by request rate. `POST /api/send-message` and `POST /api/bulk-send` are therefore not held to the default limit, and are not counted against it. A limit you set on a key yourself is a deliberate choice and applies to every call, including sends. Every response from a limited key carries the current state: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per minute for this key. | | `X-RateLimit-Remaining` | Requests left in the current window. | | `X-RateLimit-Reset` | Seconds until the window rolls. | | `Retry-After` | On a 429 only: seconds to wait before retrying. | **429 Too Many Requests** ```json { "error": "rate_limited", "message": "This key is limited to 600 requests per minute. Retry after 12s.", "retry_after_seconds": 12 } ``` - Back off for `Retry-After` seconds, then retry. Do not retry in a tight loop. - Do not poll the same endpoint faster than every 30 seconds. For "tell me when something happens", use webhooks. - Prefer one list call with filters over many single-item calls. #### Two limits, two response shapes A key is held to its own limit, and all API traffic from one IP address is also held to a platform-wide ceiling. Both answer 429, but they answer differently. Whichever you meet, back off for `Retry-After` seconds. | | Per-key limit | Per-IP ceiling | | --- | --- | --- | | Limit | 600 a minute by default, or the key’s own | 1,000 a minute per IP address | | Not applied to | Sending (`/api/send-message`, `/api/bulk-send`) on a key with no limit of its own | Sending (`/api/send-message`, `/api/bulk-send`) | | Body | `{"error":"rate_limited","message":"...","retry_after_seconds":12}` | `{"error":"Too many requests, please try again later."}` | | Headers | `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After` | `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, `RateLimit-Policy`, `Retry-After` | A per-key limit above 1,000 a minute only helps when calls come from more than one IP address, because the ceiling applies first. ### Pagination and windows (Also at https://inboxwa.online/docs/api/guide/pagination.md) Lists return `data` and a `next_cursor`. To get the next page, pass `next_cursor` back as `cursor`. Treat it as opaque. A `null` cursor means the last page. `limit` is 1 to 200; each endpoint states its default. List endpoints look at a window of time rather than all history, so one call cannot scan years of chats. `since` and `to` accept an ISO 8601 timestamp or a date. Defaults and maximum spans are stated per endpoint: 30 days for chat lists, 7 days for replies, metrics and failed messages. The window is echoed back in the response. **Walking every page** ```bash cursor="" while :; do page=$(curl -s "https://inboxwa.online/api/reporting/v1/conversations?limit=200&unread=true${cursor:+&cursor=$cursor}" \ -H "Authorization: Bearer $INBOXWA_KEY") echo "$page" | jq -c '.data[]' cursor=$(echo "$page" | jq -r '.next_cursor // empty') [ -z "$cursor" ] && break done ``` ### Errors (Also at https://inboxwa.online/docs/api/guide/errors.md) Errors are JSON, but two conventions live side by side, because this API is the web app’s own interface. Newer endpoints return a stable snake_case code in `error` and a sentence in `message`. Older endpoints return a sentence in `error` and no `message`. Each table below says which is which. #### Stable codes The Reporting API, key management, webhook settings and the checks every key goes through return a code. Match on `error`. **Error shape** ```json { "error": "scope_denied", "message": "This API key isn't scoped for inbox. Mint a new key with the right scope on the API Keys page.", "requiredPermission": "inbox", "keyScopes": ["templates"] } ``` | Status | error | When | | --- | --- | --- | | 400 | `invalid_*`, `since_after_to`, `window_too_large` | A parameter is missing or malformed. The code names it. | | 403 | `key_expired` | The key has passed its expiry. | | 403 | `read_only_key` | A read-only key attempted a write. | | 403 | `scope_denied` | The key’s scopes do not cover this endpoint, or an admin-only endpoint was called with a scoped key. | | 403 | `permission_denied` | The admin who created the key lacks the permission. | | 403 | `admin_required` | An admin-only action by someone who is not an admin. | | 403 | `api_not_in_plan` | The workspace is on Starter, which has no API. | | 403 | `api_access_not_enabled` | The call is part of the full API, which is included in the Scale plan, and the key belongs to a Growth workspace. | | 403 | `webhooks_not_in_plan` | The workspace is on Starter, which has no webhooks. | | 403 | `event_requires_scale` | A webhook subscription asks for an event that is part of the Scale plan. | | 403 | `feature_disabled_for_org` | A feature the call needs, such as the WhatsApp Cloud API, is switched off for the workspace. | | 403 | `account_suspended`, `membership_revoked` | The admin who created the key is suspended or no longer in the workspace. | | 404 | `not_found` | No such item in this workspace. A chat outside a member’s assignment also returns this. | | 412 | `version_conflict` | A contact was edited by someone else; refetch and retry. | | 429 | `rate_limited` | Over the per-key limit. See Retry-After and the Rate limits guide. | | 500 | `internal_error` | Our side. Retry with backoff; it is safe for reads. | #### Sentences, on older endpoints The sign-in check, sending, and the contact calls return the error as a sentence in `error`. The text can change, so match on the HTTP status, and for sending also on `errorCode`. Each call’s own page lists its errors. | Status | Error as returned | When | | --- | --- | --- | | 401 | Unauthorized: No token provided | No credential was sent. | | 403 | Forbidden: Invalid or revoked API key | The key does not exist or was revoked. An expired key is `key_expired` above. | | 403 | Account suspended. Contact your admin. | The admin who created the key is suspended. | | 400 | Phone number is required, Message text is required, Template name is required | A required field of a send call is missing. | | 404 | Contact not found | No such contact, or one the caller cannot see. | | 500 | An internal error occurred. Please try again. | Our side, on the contact calls. | | 500 | The reason WhatsApp gave | WhatsApp refused a send. See the next section. | #### When a send fails `POST /api/send-message` answers **500** when WhatsApp refuses the message. The reason is in `error`, and WhatsApp’s details are in `errorCode`, `errorTitle`, `errorSubcode` and `errorDetails`. Unlike other 5xx responses this is usually not a fault on our side, and sending the same request again will fail the same way, so read `errorCode` before you retry. | errorCode | What it means | What to do | | --- | --- | --- | | 131047 | More than 24 hours since the customer last wrote, so free-form text and media are not allowed. | Send a template instead. | | 131026 | The message could not be delivered to that number, for example it is not on WhatsApp. | Do not retry. Check the number. | | 132001 | The template does not exist in that language. | Fix the template name or language. | | 132000 | The number of template parameters does not match the template. | Send the right number of parameters. | | 131049 | WhatsApp declined to deliver a marketing message to protect the customer experience. | Do not retry straight away. | | 130429 | WhatsApp’s throughput limit was reached. | Retry with backoff. | These are WhatsApp’s own codes. The full list is in Meta’s Cloud API error reference. ### How the numbers are defined (Also at https://inboxwa.online/docs/api/guide/concepts.md) #### Who sent a message Every message has a `sender_type`. `customer` is the contact. `bot` is the AI agent. `agent` is a team member, recorded on messages sent from InboxWA along with their name. `system` is any other outbound message: an automation, a campaign, a template, or an older message with no recorded author. A bot reply is never reported as a human. #### Two clocks for response time A bot usually answers in seconds, so a single response time hides what a team lead wants to know: how long until a person answered. A bot or system reply is timed from when the customer began waiting. An agent reply is timed from when the customer began waiting for a person. `first_human_response` is the figure to hold a team to. > **A failed send is not a reply** A message that failed to send never reached the customer, so it does not stop a wait, does not count as a first response, and does not make a chat "replied to". #### What is recorded, and from when - **Receipts.** Delivery and read times are recorded from the day recording began (the `recorded_from` field says when). Earlier messages keep only their latest status. - **Read receipts** depend on the customer. WhatsApp sends a read receipt only when the customer has read receipts switched on. - **Seen-by** means a team member opened the chat in the InboxWA inbox or the embedded panel, not that they read it on a phone. A key marking a chat read is an integration and is not recorded as a person looking. - **Assignment history** is exact from `detailed_since`; before that it is rebuilt from the team audit log, which has claims, releases and transfers but not resolutions. ### Webhooks (Also at https://inboxwa.online/docs/api/guide/webhooks.md) Register one HTTPS endpoint per workspace and choose the events. Webhooks are part of Growth (five core events) and Scale (all events); Starter has none. Each event is a POST with a JSON body in this envelope: **Envelope** ```json { "event": "message_status", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { } } ``` | Header | Value | | --- | --- | | `X-Webhook-Event` | The event name. | | `X-Webhook-Timestamp` | Unix seconds when it was sent. | | `X-Correlation-Id` | Same as correlation_id. Unique per event; use it to de-duplicate. | | `X-Webhook-Signature` | `sha256=` followed by the hex HMAC-SHA256 of the raw request body, keyed with your signing secret. | #### Verify the signature Compute the HMAC over the exact bytes you received, before any JSON parsing, and compare in constant time. Reject the request if it does not match. The signature covers the body only. The `X-Webhook-Timestamp` header is not signed, so do not rely on it. **Verifying** ##### Node.js ```javascript const crypto = require("crypto"); // rawBody must be the exact bytes received (express.raw), not re-serialised JSON function verify(rawBody, header, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); const a = Buffer.from(header || ""); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ##### Python ```python import hmac, hashlib def verify(raw_body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(header or "", expected) ``` ##### PHP ```php function verify(string $raw, ?string $header, string $secret): bool { $expected = "sha256=" . hash_hmac("sha256", $raw, $secret); return hash_equals($expected, $header ?? ""); } ``` #### Reject replays A valid request that someone captured can be sent to you again. `sent_at` sits inside the signed body, so it cannot be altered without breaking the signature. Reject an event whose `sent_at` is more than five minutes old, and remember each `correlation_id` you have processed so a repeat is ignored. **After the signature has been verified** ```javascript const event = JSON.parse(rawBody); const age = Date.now() - Date.parse(event.sent_at); if (!(age > -60_000 && age < 5 * 60_000)) return res.status(400).end(); // too old, or from the future if (await alreadyProcessed(event.correlation_id)) return res.status(200).end(); // a repeat await markProcessed(event.correlation_id); // a database or Redis in production ``` #### Delivery - Answer with any 2xx quickly. Do slow work after responding. - Up to three attempts: the first at once, then after waits of 1 and 4 seconds, on network errors and 5xx responses only. A 4xx is a rejection and is not retried. Each attempt times out after 8 seconds. Nothing is retried after that. - **An event that fails every attempt is not delivered again.** If your endpoint can be down for longer than a few seconds, look for failures in the delivery log (`GET /api/webhook/outbound/deliveries`), and catch up by reading the same information from the API, for example `GET /api/reporting/v1/messages` with `since` set to when the outage began (Scale plan). - Delivery happens after the message is handled, so a slow endpoint never delays a WhatsApp reply. - Events can arrive more than once and out of order. De-duplicate on `correlation_id` and order by the timestamps in `data`. - Use `POST /api/webhook/outbound/test` to send a signed test event, and `GET /api/webhook/outbound/deliveries` to see what your endpoint answered. - Rotate the signing secret with `POST /api/webhook/outbound/rotate-secret`. The new secret is shown once. ### Integrations (Also at https://inboxwa.online/docs/api/guide/integrations.md) You do not need to write code to use your key. The same key connects InboxWA to the tools most teams already use. Zapier and Make connect with a key, so they are part of Growth and Scale. - **Zapier** triggers Zaps when messages arrive and sends WhatsApp messages from other apps. [Zapier guide](https://inboxwa.online/docs/zapier). - **Make** builds multi-step scenarios with HTTP modules. [Make guide](https://inboxwa.online/docs/make). - **Inbound webhooks** let another system push events into InboxWA. [Webhooks guide](https://inboxwa.online/docs/webhooks) and [Webhook builder](https://inboxwa.online/docs/webhook-builder). - **Shopify, Google Sheets and Razorpay** have their own setup pages: [Shopify](https://inboxwa.online/docs/shopify), [Google Sheets](https://inboxwa.online/docs/google-sheets), [Razorpay](https://inboxwa.online/docs/razorpay). - **WHMCS** connects hosting billing and support to the inbox. [WHMCS connector](https://inboxwa.online/docs/whmcs-connector). ### AI tools (MCP) (Also at https://inboxwa.online/docs/api/guide/mcp.md) `POST /api/mcp` is a Model Context Protocol endpoint (JSON-RPC 2.0 over plain HTTP, no session) that lets any MCP client, such as Claude Desktop or Cursor, list and call the workspace actions you have chosen to expose. - **It does not use an API key.** It has its own bearer token, created in the Server Actions settings, or a token from the OAuth connection flow. A missing or invalid token is answered 401 `invalid_token`. - **Only exposed actions are visible.** An action has to be marked as exposed to MCP, and an action that requires approval in the app cannot be exposed. - Only `tools/list` and `tools/call` are supported. ## Reference ### Messaging Send a message to one contact, or a broadcast to many, through the WhatsApp Cloud API, and list your templates. Part of the core API (Growth and Scale). Sending needs the whatsapp_api scope and is not held to the default per-key request limit. #### Send a message `POST /api/send-message` (operationId: `sendMessage`, group: Messaging) Sends one message to one contact through the WhatsApp Cloud API: text, a template, or an image, video, audio or document. Free-form text and media can only be sent within 24 hours of the customer’s last message; outside that window send a template. For a chat imported from WhatsApp Web only text is supported, and the response carries channel: whatsapp_web. Subject to the workspace’s consent, assignment and channel rules. Not held to the default per-key request limit. A 500 here usually means WhatsApp refused the message, not that we failed: read errorCode before retrying (see the Errors guide). Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `whatsapp_api`. The workspace must also have the WhatsApp Cloud API enabled (403 feature_disabled_for_org otherwise). **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `to` | string | yes | Recipient phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | | `type` | string | no | Default text. One of: `text`, `template`, `image`, `video`, `audio`, `document`. | | `message` | string | no | The text, when type is text. | | `template` | object | no | When type is template. | | `template.name` | string | yes | Approved template name. Required. | | `template.language` | string | no | Language code such as en_US. Looked up from the approved template when omitted. | | `template.parameters` | array of string | no | Values for the body variables, in order. | | `template.headerImage` | string | no | Public URL of a header image. | | `template.headerImageId` | string | no | Uploaded media id of a header image. | | `mediaUrl` | string | no | Public URL of the media, for image, video, audio and document. Send this or mediaId. | | `mediaId` | string | no | A media id you uploaded to WhatsApp. Send this or mediaUrl. | | `caption` | string | no | Caption for image, video and document. | | `filename` | string | no | File name shown for a document. | | `senderId` | string | no | WhatsApp phone number id to send from. Default: the workspace’s main number. | | `confirmPolicy` | string | no | Send again with inbox.reply.sensitive_guard after a 403 policy_confirm, once the person has confirmed the text is meant to go out. | Example: ```json { "to": "919876543210", "type": "template", "template": { "name": "order_confirmation", "language": "en_US", "parameters": [ "ORD-4271", "2026-10-05" ] } } ``` **Response 200**: Accepted by WhatsApp. Delivery and read progress arrive as message_status webhooks, or read them from the receipts calls. Example: ```json { "success": true, "messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSQTEB", "data": { "messaging_product": "whatsapp", "contacts": [ { "input": "919876543210", "wa_id": "919876543210" } ], "messages": [ { "id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSQTEB" } ] } } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `Phone number is required` | to is missing. | | 400 | `Message text is required` | type is text and message is empty. | | 400 | `Template name is required` | type is template and template.name is missing. | | 400 | `mediaId or mediaUrl is required for media messages` | A media type without media. | | 403 | `feature_disabled_for_org` | The WhatsApp Cloud API is switched off for the workspace. | | 403 | `not_assigned` | The caller is limited to assigned chats and this one is not assigned to them. | | 403 | `policy_denied` | A workspace policy refused the send: someone else is handling the chat (reply lock), the file type is not allowed, the text contains sensitive data the workspace blocks, or the sender holds the most chats allowed. policy names the rule and message says why. | | 403 | `policy_confirm` | The text looks like it contains a card, Aadhaar or PAN number and the workspace asks for confirmation. Send again with confirmPolicy set to the policy named in the reply. | | 500 | `WhatsApp’s own message` | WhatsApp refused the send. errorCode, errorTitle, errorSubcode and errorDetails carry its reason, for example 131047 for a closed 24-hour window. Retrying the same request will fail the same way. | (Also at https://inboxwa.online/docs/api/op/sendMessage.md) #### Send a broadcast `POST /api/bulk-send` (operationId: `bulkSend`, group: Messaging) Sends one message or template to many numbers as a campaign. Numbers that are blacklisted, opted out, or over the workspace’s frequency cap are skipped and counted in the response. The call answers 202 at once and the sending runs in the background, one recipient at a time with a delay between them; a broadcast can also be scheduled, or held for approval when the workspace requires it. Not held to the default per-key request limit; volume is governed by your credits and WhatsApp’s own limits. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `whatsapp_api`. The workspace must also have the WhatsApp Cloud API enabled. **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `numbers` | array of string | yes | Recipient phone numbers. | | `type` | string | no | Default text. One of: `text`, `template`, `image`, `video`, `document`. | | `message` | string | no | The text or caption. | | `template` | object | no | Template name, language and header, as for a single send. | | `variableMapping` | object | no | Maps a template variable position to a contact field, for example {"1": "contact.name"}. | | `mediaUrl` | string | no | Public URL of the media. Required for image, video and document. | | `campaignName` | string | no | A name to find the campaign by. | | `scheduledAt` | string (date-time) | no | Send later, at this time. | | `senderId` | string | no | WhatsApp phone number id to send from. | | `trackLinks` | boolean | no | Track clicks on links in the message. | | `delayMs` | integer | no | Pause between recipients. Default 1000. Range 250..5000. | | `enableRetry` | boolean | no | Retry a failed recipient. Default true. | | `maxRetries` | integer | no | Retries per recipient. Default 2. Range 0..5. | Example: ```json { "numbers": [ "919876543210", "919812345678" ], "type": "template", "template": { "name": "diwali_offer", "language": "en_US" }, "campaignName": "Diwali offer", "variableMapping": { "1": "contact.name" } } ``` **Response 200**: Accepted (202). status is processing, scheduled (with scheduledAt), or pending_approval. Example: ```json { "success": true, "status": "processing", "campaignId": 3812, "message": "Broadcast accepted and is executing in the background.", "totalRecipients": 2, "skippedOptOut": 0, "skippedBlacklist": 0 } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `Phone numbers array is required` | numbers is missing or empty. | | 400 | `mediaUrl is required for media broadcasts` | An image, video or document broadcast without mediaUrl. | | 400 | `All recipients are blacklisted, opted out, or over the frequency cap` | Nothing left to send. skippedOptOut, skippedBlacklist and skippedFrequencyCap say why. | | 403 | `feature_disabled_for_org` | The WhatsApp Cloud API is switched off for the workspace. | (Also at https://inboxwa.online/docs/api/op/bulkSend.md) #### List message templates `GET /api/templates` (operationId: `listTemplates`, group: Messaging) The workspace’s WhatsApp message templates as WhatsApp holds them, up to 100, each with its name, status, category, language and components. To send a template message, use the name and language of a template whose status is APPROVED. Part of the core API, included in the Growth and Scale plans. Any valid key, on Growth and Scale. The workspace must have WhatsApp connected. **Response 200**: The templates. Example: ```json { "success": true, "templates": [ { "id": "1234567890123456", "name": "order_confirmation", "status": "APPROVED", "category": "UTILITY", "language": "en_US", "components": [ { "type": "BODY", "text": "Your order {{1}} ships on {{2}}." } ] } ], "total": 1 } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 403 | `not_connected` | WhatsApp is not connected for this workspace. | | 500 | `Failed to fetch templates` | WhatsApp did not answer. | (Also at https://inboxwa.online/docs/api/op/listTemplates.md) ### Contacts Create and update contacts, their tags and custom attributes, and their consent: part of the core API (Growth and Scale). Reading contacts, marking a chat read, and deleting contacts are part of the full API (Scale plan). #### List contacts `GET /api/contacts/paginated` (operationId: `listContacts`, group: Contacts) Contacts, newest activity first, with pagination. Each contact has phone, name, email, company, status (New by default), tags (one comma-separated string), notes, chat_status, custom_attributes (an object), last_msg_time, claimed_by and claimed_by_name, plus the columns the app shows. search matches name, phone, tags and message text. For an API key this is part of the full API (Scale plan). Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`, `contacts:read`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | no | Page number from 1. Default 1. | | `limit` | query | integer | no | Page size. Default 50. Default 50. | | `search` | query | string | no | Search name, phone, tags and message text. | **Response 200**: A page of contacts. Example: ```json { "success": true, "contacts": [ { "phone": "919876543210", "name": "Ravi Kumar", "email": "ravi@example.com", "company": "Kumar Hosting", "status": "New", "tags": "vip,hosting", "chat_status": "open", "custom_attributes": { "plan": "Business" }, "last_msg_time": "2026-10-02T09:06:40.000Z" } ], "meta": { "total": 14970, "page": 1, "limit": 50, "hasMore": true } } ``` (Also at https://inboxwa.online/docs/api/op/listContacts.md) #### Search contacts `GET /api/contacts/search` (operationId: `searchContacts`, group: Contacts) Finds contacts by name, phone number or tag. Returns up to 50. An empty q returns an empty list. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`, `contacts:read`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `q` | query | string | yes | The text to look for. | **Response 200**: Matches. Example: ```json { "success": true, "contacts": [ { "phone": "919876543210", "name": "Ravi Kumar", "tags": "vip,hosting" } ], "total": 1 } ``` (Also at https://inboxwa.online/docs/api/op/searchContacts.md) #### List contacts (plain array) `GET /api/contacts` (operationId: `listAllContacts`, group: Contacts) Contacts as a plain JSON array rather than a page object, with an optional search and limit. Use List contacts for anything large. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`, `contacts:read`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `search` | query | string | no | Search name, phone and tags. | | `limit` | query | integer | no | Maximum contacts to return. | **Response 200**: A JSON array of contacts. Example: ```json [ { "phone": "919876543210", "name": "Ravi Kumar", "status": "New", "tags": "vip,hosting" } ] ``` (Also at https://inboxwa.online/docs/api/op/listAllContacts.md) #### Create or update a contact `POST /api/contacts` (operationId: `createContact`, group: Contacts) Saves a contact by phone number. A number that already exists is updated, and any extra fields you send are merged into its custom attributes. The phone number is normalised and must be a valid mobile number. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `phone` | string | yes | The contact’s phone number with the country code. A 10-digit number with no country code is treated as Indian (91 is added). | | `name` | string | no | Display name. | | `email` | string | no | Email address. | | `company` | string | no | Company. | | `tags` | string | no | Comma-separated tags, for example "vip,hosting". | | `status` | string | no | CRM pipeline status. | Example: ```json { "phone": "919876543210", "name": "Ravi Kumar", "email": "ravi@example.com", "company": "Kumar Hosting", "tags": "vip,hosting" } ``` **Response 200**: Saved. Example: ```json { "success": true, "message": "Contact successfully saved." } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `Phone number is required.` | phone is missing. | | 400 | `too short` | The phone number is not valid. The reason is one of empty, no digits, too short, too long or not a mobile. | (Also at https://inboxwa.online/docs/api/op/createContact.md) #### Set a contact’s name `PUT /api/contacts/{phone}/name` (operationId: `updateContactName`, group: Contacts) Sets the contact’s display name. Send version (an integer, from an earlier response) to make the write conditional: if someone else changed the contact meanwhile the call answers 412 version_conflict with the current value instead of overwriting it, and a successful versioned write returns the new version and an ETag. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | Name. | | `version` | integer | no | Optional. The version you last saw, for a conditional write. Range 1... | Example: ```json { "name": "Ravi Kumar" } ``` **Response 200**: Saved. Example: ```json { "success": true, "name": "Ravi Kumar" } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `Name is required` | name is empty. | | 412 | `version_conflict` | Someone else changed the contact since the version you sent. | (Also at https://inboxwa.online/docs/api/op/updateContactName.md) #### Set a contact’s status `PUT /api/contacts/{phone}/status` (operationId: `updateContactStatus`, group: Contacts) Moves the contact to a CRM pipeline status. Send version (an integer, from an earlier response) to make the write conditional: if someone else changed the contact meanwhile the call answers 412 version_conflict with the current value instead of overwriting it, and a successful versioned write returns the new version and an ETag. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | Status. | | `version` | integer | no | Optional. The version you last saw, for a conditional write. Range 1... | Example: ```json { "status": "Qualified" } ``` **Response 200**: Saved. Example: ```json { "success": true, "status": "Qualified" } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `Status is required` | status is empty. | | 412 | `version_conflict` | Someone else changed the contact since the version you sent. | (Also at https://inboxwa.online/docs/api/op/updateContactStatus.md) #### Set a contact’s email `PUT /api/contacts/{phone}/email` (operationId: `updateContactEmail`, group: Contacts) Sets the email address. An empty value clears it. Send version (an integer, from an earlier response) to make the write conditional: if someone else changed the contact meanwhile the call answers 412 version_conflict with the current value instead of overwriting it, and a successful versioned write returns the new version and an ETag. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | no | Email. | | `version` | integer | no | Optional. The version you last saw, for a conditional write. Range 1... | Example: ```json { "email": "ravi@example.com" } ``` **Response 200**: Saved. Example: ```json { "success": true, "email": "ravi@example.com" } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 412 | `version_conflict` | Someone else changed the contact since the version you sent. | (Also at https://inboxwa.online/docs/api/op/updateContactEmail.md) #### Set a contact’s company `PUT /api/contacts/{phone}/company` (operationId: `updateContactCompany`, group: Contacts) Sets the company. An empty value clears it. Send version (an integer, from an earlier response) to make the write conditional: if someone else changed the contact meanwhile the call answers 412 version_conflict with the current value instead of overwriting it, and a successful versioned write returns the new version and an ETag. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `company` | string | no | Company. | | `version` | integer | no | Optional. The version you last saw, for a conditional write. Range 1... | Example: ```json { "company": "Kumar Hosting" } ``` **Response 200**: Saved. Example: ```json { "success": true, "company": "Kumar Hosting" } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 412 | `version_conflict` | Someone else changed the contact since the version you sent. | (Also at https://inboxwa.online/docs/api/op/updateContactCompany.md) #### Set a contact’s tags `PUT /api/contacts/{phone}/tags` (operationId: `updateContactTags`, group: Contacts) Replaces the contact’s tags with this comma-separated list. Newly added tags fire the Tag Added automation trigger. Send version (an integer, from an earlier response) to make the write conditional: if someone else changed the contact meanwhile the call answers 412 version_conflict with the current value instead of overwriting it, and a successful versioned write returns the new version and an ETag. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `tags` | string | no | Tags. | | `version` | integer | no | Optional. The version you last saw, for a conditional write. Range 1... | Example: ```json { "tags": "vip,hosting" } ``` **Response 200**: Saved. Example: ```json { "success": true, "tags": "vip,hosting" } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 412 | `version_conflict` | Someone else changed the contact since the version you sent. | (Also at https://inboxwa.online/docs/api/op/updateContactTags.md) #### Set a contact’s notes `PUT /api/contacts/{phone}/notes` (operationId: `updateContactNotes`, group: Contacts) Sets the internal notes. An empty value clears them. Send version (an integer, from an earlier response) to make the write conditional: if someone else changed the contact meanwhile the call answers 412 version_conflict with the current value instead of overwriting it, and a successful versioned write returns the new version and an ETag. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `notes` | string | no | Notes. | | `version` | integer | no | Optional. The version you last saw, for a conditional write. Range 1... | Example: ```json { "notes": "Asked for a callback on Monday." } ``` **Response 200**: Saved. Example: ```json { "success": true, "notes": "Asked for a callback on Monday." } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 412 | `version_conflict` | Someone else changed the contact since the version you sent. | (Also at https://inboxwa.online/docs/api/op/updateContactNotes.md) #### Get custom attributes `GET /api/contacts/{phone}/attributes` (operationId: `getContactAttributes`, group: Contacts) The contact’s custom attributes: the free-form fields you store beyond name, email, company and tags. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`, `contacts:read`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: The attributes object. Example: ```json { "success": true, "attributes": { "plan": "Business", "renewal": "2026-12-01" } } ``` (Also at https://inboxwa.online/docs/api/op/getContactAttributes.md) #### Merge custom attributes `PUT /api/contacts/{phone}/attributes` (operationId: `setContactAttributes`, group: Contacts) Merges the given attributes into the contact’s existing ones. Keys you leave out are kept; use Delete a custom attribute to remove one. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `attributes` | object | yes | Key and value pairs to merge in. | Example: ```json { "attributes": { "plan": "Business", "renewal": "2026-12-01" } } ``` **Response 200**: The merged attributes. Example: ```json { "success": true, "attributes": { "plan": "Business", "renewal": "2026-12-01" } } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `attributes object is required` | attributes is missing or not an object. | (Also at https://inboxwa.online/docs/api/op/setContactAttributes.md) #### Delete a custom attribute `DELETE /api/contacts/{phone}/attributes/{key}` (operationId: `deleteContactAttribute`, group: Contacts) Removes one custom attribute from the contact and returns what is left. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | | `key` | path | string | yes | The attribute name to remove. | **Response 200**: The remaining attributes. Example: ```json { "success": true, "attributes": { "plan": "Business" } } ``` (Also at https://inboxwa.online/docs/api/op/deleteContactAttribute.md) #### Mark a contact opted out `POST /api/contacts/{phone}/opt-out` (operationId: `optOutContact`, group: Contacts) Marks the contact as opted out of broadcasts and records a consent event. Broadcasts skip opted-out numbers. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: Recorded. Example: ```json { "success": true } ``` (Also at https://inboxwa.online/docs/api/op/optOutContact.md) #### Re-subscribe a contact `POST /api/contacts/{phone}/opt-in` (operationId: `optInContact`, group: Contacts) Clears the opted-out flag and records a consent event. Only do this when the contact has actually asked to hear from you again. Part of the core API, included in the Growth and Scale plans. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: Recorded. Example: ```json { "success": true } ``` (Also at https://inboxwa.online/docs/api/op/optInContact.md) #### List opted-out contacts `GET /api/contacts/opted-out` (operationId: `listOptedOut`, group: Contacts) Every contact currently opted out of broadcasts, most recently changed first. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`, `contacts:read`. **Response 200**: Opted-out contacts. Example: ```json { "success": true, "data": [ { "phone": "919812345678", "name": "Asha Rao", "tags": "newsletter", "updated_at": "2026-09-30T11:02:00.000Z" } ], "count": 1 } ``` (Also at https://inboxwa.online/docs/api/op/listOptedOut.md) #### Mark a chat read `POST /api/contacts/{phone}/read` (operationId: `markRead`, group: Contacts) Marks every unread customer message in the chat as read. A dashboard session also records that the signed-in team member opened the chat (seen-by). A key marking a chat read is an integration, so nothing is recorded as a person looking. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: Marked. Example: ```json { "success": true } ``` (Also at https://inboxwa.online/docs/api/op/markRead.md) #### Delete a contact `DELETE /api/contacts/{phone}` (operationId: `deleteContact`, group: Contacts) Permanently deletes the contact and its chat data. This cannot be undone. A caller limited to assigned chats can only delete contacts assigned to them. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: Deleted. Example: ```json { "success": true, "deleted": true } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 404 | `Contact not found` | No such contact, or not one the caller can see. | (Also at https://inboxwa.online/docs/api/op/deleteContact.md) #### Delete many contacts `POST /api/contacts/bulk-delete` (operationId: `bulkDeleteContacts`, group: Contacts) Deletes up to 500 contacts in one call. Contacts the caller cannot see are left alone and are not counted in deleted. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `contacts`. **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `phones` | array of string | yes | Phone numbers, 1 to 500. | Example: ```json { "phones": [ "919876543210", "919812345678" ] } ``` **Response 200**: How many were deleted out of how many were asked for. Example: ```json { "success": true, "deleted": 2, "total": 2 } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `phones array required` | phones is missing or empty. | | 400 | `max 500 phones per call` | More than 500 numbers. | (Also at https://inboxwa.online/docs/api/op/bulkDeleteContacts.md) ### Reporting Purpose-built reads for the accountability questions: unread, unanswered, seen-by, receipts, replies, first response and assignment history. Part of the full API (Scale plan), except Check a key. #### Check a key `GET /api/reporting/v1/whoami` (operationId: `whoami`, group: Reporting) Reports which workspace the credential belongs to, its scopes, whether it is read-only, and what the plan includes: api_tier is core on Growth (the core API) and full on Scale (every call). Costs no data, so it is the first call to make when wiring an integration. Part of the core API, included in the Growth and Scale plans. Any valid key, on Growth and Scale. **Response 200**: OK (schema `Whoami`) | Field | Type | Description | | --- | --- | --- | | `workspace_id` | integer | | | `auth` | string | One of: `api_key`, `session`. | | `key` | object, nullable | | | `key.id` | integer, nullable | | | `key.scopes` | array of string | | | `key.read_only` | boolean | | | `api_access` | boolean | True means the full API (Scale plan). False means the core API only, or none. | | `api_tier` | string | What the plan includes: none (Starter), core (Growth), full (Scale). One of: `none`, `core`, `full`. | | `reporting_api` | string | Reporting API version. | Example: ```json { "workspace_id": 195, "auth": "api_key", "key": { "id": 7, "scopes": [ "inbox:read", "team_management:read" ], "read_only": true }, "api_access": true, "api_tier": "full", "reporting_api": "v1" } ``` (Also at https://inboxwa.online/docs/api/op/whoami.md) #### List conversations `GET /api/reporting/v1/conversations` (operationId: `listConversations`, group: Reporting) Chats active in the window, newest first, each with unread count, who spoke last, state, assignee and SLA deadline. Filters combine. The window defaults to the last 30 days so one call cannot scan a whole history; counts are measured inside it. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `unread` | query | boolean | no | Only chats with customer messages nobody has opened. | | `needs_reply` | query | boolean | no | Only chats where the customer is waiting: they spoke last and no delivered reply followed. Failed sends do not count as replies. | | `state` | query | string | no | Only chats in this state. One of: `AI_ONLY`, `HANDOFF_PENDING`, `HUMAN_ACTIVE`. | | `assigned_to` | query | integer | no | Only chats assigned to this team member (user id). | | `sla_breached` | query | boolean | no | Only chats waiting for a person past their SLA deadline. | | `q` | query | string | no | Search contact name or phone number. | | `since` | query | string (date-time) | no | Start of the window: an ISO 8601 timestamp or a date (YYYY-MM-DD, start of that UTC day). Default: 30 days ago. Alias: from. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `sort` | query | string | no | last_activity: newest first. waiting_longest: customers who have waited longest first. One of: `last_activity`, `waiting_longest`. Default last_activity. | | `limit` | query | integer | no | Page size, 1 to 200. Default 50. Default 50. | | `cursor` | query | string | no | The next_cursor from the previous page. Treat it as opaque. | **Response 200**: OK (schema `ConversationList`) | Field | Type | Description | | --- | --- | --- | | `data` | array of ConversationSummary | | | `data[][].phone` | string | Contact phone number, digits only, as stored. | | `data[][].contact_name` | string, nullable | Contact name if known. | | `data[][].state` | string | AI_ONLY: the bot is handling it. HANDOFF_PENDING: waiting for a person. HUMAN_ACTIVE: a person has it. One of: `AI_ONLY`, `HANDOFF_PENDING`, `HUMAN_ACTIVE`. | | `data[][].handover_reason` | string, nullable | Why it was handed to a person. | | `data[][].assigned_to` | object, nullable | | | `data[][].claimed_by` | object, nullable | | | `data[][].unread_count` | integer | Customer messages nobody has opened, inside the window. | | `data[][].needs_reply` | boolean | The customer spoke last (or, when the workspace has the unattended-AI-reply setting, the AI did) and no delivered reply has followed. | | `data[][].last_message` | object, nullable | | | `data[][].last_message.message_id` | string, nullable | WhatsApp message id. | | `data[][].last_message.sender_type` | string, nullable | customer: the contact. bot: the AI agent. agent: a team member (recorded on messages sent from InboxWA). system: any other outbound message (automation, campaign, template, or an older message with no recorded author). A bot reply is never reported as a human. One of: `customer`, `bot`, `agent`, `system`. | | `data[][].last_message.sent_at` | string (date-time) | When it was sent. | | `data[][].last_message.preview` | string, nullable | First 140 characters. | | `data[][].waiting_since` | string (date-time), nullable | When the customer began waiting. Null unless needs_reply. | | `data[][].waiting_seconds` | integer, nullable | Seconds the customer has been waiting. | | `data[][].sla_deadline_at` | string (date-time), nullable | SLA response deadline, if a policy applies. | | `data[][].sla_breached` | boolean | Deadline passed while still waiting for a person. | | `data[][].labels` | array of string | | | `data[][].counts` | object | | | `data[][].counts.messages` | integer | | | `data[][].counts.from_customer` | integer | | | `data[][].counts.from_bot` | integer | | | `next_cursor` | string, nullable | Pass as cursor to get the next page. Null on the last page. | | `window` | Window | | | `window.from` | string (date-time) | Start of the window. | | `window.to` | string (date-time) | End of the window. | Example: ```json { "data": [ { "phone": "919876543210", "contact_name": "Ravi Kumar", "state": "HUMAN_ACTIVE", "handover_reason": "human_claimed", "assigned_to": { "user_id": 41, "name": "Anita" }, "claimed_by": { "user_id": 41, "name": "Anita" }, "unread_count": 3, "needs_reply": true, "last_message": { "message_id": "wamid.HBgM...C3", "sender_type": "customer", "sent_at": "2026-10-02T09:06:40.000Z", "preview": "Server is still down, please check" }, "waiting_since": "2026-10-02T09:06:40.000Z", "waiting_seconds": 1260, "sla_deadline_at": null, "sla_breached": false, "labels": [ "vip" ], "counts": { "messages": 5, "from_customer": 3, "from_bot": 1 } } ], "next_cursor": "eyJrIjoidHMiLCJ0cyI6IjIwMjYtMTAtMDJUMDk6MDY6NDAuMDAwWiIsInBob25lIjoiOTE5ODc2NTQzMjEwIn0", "window": { "from": "2026-09-02T00:00:00.000Z", "to": "2026-10-02T09:30:00.000Z" } } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_state` | state is not one of the three states. | | 400 | `invalid_assigned_to` | assigned_to is not a user id. | | 400 | `invalid_sort` | sort is not recognised. | | 400 | `invalid_since` | since or to is not a date. | | 400 | `window_too_large` | The window is longer than 400 days. | (Also at https://inboxwa.online/docs/api/op/listConversations.md) #### Unread and waiting totals `GET /api/reporting/v1/conversations/unread-summary` (operationId: `unreadSummary`, group: Reporting) Workspace totals: how many chats have unread messages, how many customers are waiting for a reply, the longest wait, and the same split by assignee. Aggregates only; no phone numbers. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `since` | query | string (date-time) | no | Start of the window: an ISO 8601 timestamp or a date (YYYY-MM-DD, start of that UTC day). Default: 30 days ago. Alias: from. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | **Response 200**: OK (schema `UnreadSummary`) | Field | Type | Description | | --- | --- | --- | | `window` | Window | | | `window.from` | string (date-time) | Start of the window. | | `window.to` | string (date-time) | End of the window. | | `unread_chats` | integer | | | `unread_messages` | integer | | | `oldest_unread_at` | string (date-time), nullable | Oldest unread customer message. | | `waiting_chats` | integer | Chats where the customer is waiting for a reply. | | `oldest_waiting_since` | string (date-time), nullable | When the longest-waiting customer began waiting. | | `longest_wait_seconds` | integer, nullable | | | `by_assignee` | array of object | | | `by_assignee[][].assigned_to` | MemberRef | | | `by_assignee[][].assigned_to.user_id` | integer | Team member user id. | | `by_assignee[][].assigned_to.name` | string, nullable | Display name. | | `by_assignee[][].unread_chats` | integer | | | `by_assignee[][].unread_messages` | integer | | | `by_assignee[][].oldest_unread_at` | string (date-time), nullable | | | `by_assignee[][].waiting_chats` | integer | | | `by_assignee[][].oldest_waiting_since` | string (date-time), nullable | | | `unassigned` | object | | | `unassigned.assigned_to` | object, nullable | | | `unassigned.unread_chats` | integer | | | `unassigned.unread_messages` | integer | | | `unassigned.oldest_unread_at` | string (date-time), nullable | | | `unassigned.waiting_chats` | integer | | | `unassigned.oldest_waiting_since` | string (date-time), nullable | | Example: ```json { "window": { "from": "2026-09-02T00:00:00.000Z", "to": "2026-10-02T09:30:00.000Z" }, "unread_chats": 12, "unread_messages": 31, "oldest_unread_at": "2026-10-01T04:12:00.000Z", "waiting_chats": 9, "oldest_waiting_since": "2026-10-01T04:12:00.000Z", "longest_wait_seconds": 105480, "by_assignee": [ { "assigned_to": { "user_id": 41, "name": "Anita" }, "unread_chats": 5, "unread_messages": 14, "oldest_unread_at": "2026-10-02T06:30:00.000Z", "waiting_chats": 4, "oldest_waiting_since": "2026-10-02T06:30:00.000Z" } ], "unassigned": { "assigned_to": null, "unread_chats": 7, "unread_messages": 17, "oldest_unread_at": "2026-10-01T04:12:00.000Z", "waiting_chats": 5, "oldest_waiting_since": "2026-10-01T04:12:00.000Z" } } ``` (Also at https://inboxwa.online/docs/api/op/unreadSummary.md) #### Unanswered conversations `GET /api/reporting/v1/conversations/unanswered` (operationId: `unansweredConversations`, group: Reporting) Chats where the customer has been waiting longer than older_than, longest wait first. Accepts the same filters as List conversations. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `older_than` | query | string | no | Minimum wait: a number with a unit, such as 30m, 2h or 1d. Default 30m. Up to 90 days. Default 30m. | | `state` | query | string | no | Only chats in this state. One of: `AI_ONLY`, `HANDOFF_PENDING`, `HUMAN_ACTIVE`. | | `assigned_to` | query | integer | no | Only chats assigned to this team member. | | `q` | query | string | no | Search contact name or phone number. | | `since` | query | string (date-time) | no | Start of the window: an ISO 8601 timestamp or a date (YYYY-MM-DD, start of that UTC day). Default: 30 days ago. Alias: from. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `limit` | query | integer | no | Page size, 1 to 200. Default 50. Default 50. | | `cursor` | query | string | no | The next_cursor from the previous page. Treat it as opaque. | **Response 200**: OK (schema `UnansweredList`) Example: ```json { "data": [ { "phone": "919876543210", "contact_name": "Ravi Kumar", "state": "HUMAN_ACTIVE", "handover_reason": "human_claimed", "assigned_to": { "user_id": 41, "name": "Anita" }, "claimed_by": { "user_id": 41, "name": "Anita" }, "unread_count": 3, "needs_reply": true, "last_message": { "message_id": "wamid.HBgM...C3", "sender_type": "customer", "sent_at": "2026-10-02T09:06:40.000Z", "preview": "Server is still down, please check" }, "waiting_since": "2026-10-02T09:06:40.000Z", "waiting_seconds": 1260, "sla_deadline_at": null, "sla_breached": false, "labels": [ "vip" ], "counts": { "messages": 5, "from_customer": 3, "from_bot": 1 } } ], "next_cursor": null, "window": { "from": "2026-09-02T00:00:00.000Z", "to": "2026-10-02T09:30:00.000Z" }, "older_than_seconds": 1800 } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_older_than` | older_than is not a duration. | (Also at https://inboxwa.online/docs/api/op/unansweredConversations.md) #### Get a conversation `GET /api/reporting/v1/conversations/{phone}` (operationId: `getConversation`, group: Reporting) One chat, over its whole life (no window), with contact details and hand-over timestamps. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: OK (schema `ConversationDetail`) Example: ```json { "phone": "919876543210", "contact_name": "Ravi Kumar", "state": "HUMAN_ACTIVE", "handover_reason": "human_claimed", "assigned_to": { "user_id": 41, "name": "Anita" }, "claimed_by": { "user_id": 41, "name": "Anita" }, "unread_count": 3, "needs_reply": true, "last_message": { "message_id": "wamid.HBgM...C3", "sender_type": "customer", "sent_at": "2026-10-02T09:06:40.000Z", "preview": "Server is still down, please check" }, "waiting_since": "2026-10-02T09:06:40.000Z", "waiting_seconds": 1260, "sla_deadline_at": null, "sla_breached": false, "labels": [ "vip" ], "counts": { "messages": 5, "from_customer": 3, "from_bot": 1 }, "handover_at": "2026-10-02T09:01:00.000Z", "claimed_at": "2026-10-02T09:04:30.000Z", "resolved_at": null, "contact": { "name": "Ravi Kumar", "email": "ravi@example.com", "company": "Kumar Hosting", "status": "New", "tags": [ "vip", "hosting" ] } } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 404 | `not_found` | No such chat in this workspace, or it is not assigned to a member limited to assigned chats. | (Also at https://inboxwa.online/docs/api/op/getConversation.md) #### List messages in a conversation `GET /api/reporting/v1/conversations/{phone}/messages` (operationId: `listMessages`, group: Reporting) The thread. Every message says who sent it (customer, bot, agent or system), which team member if an agent, and its delivery status. Add expand=receipts to attach the time of each delivery and read tick to outbound messages. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | | `since` | query | string (date-time) | no | Only messages at or after this time. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `order` | query | string | no | asc: oldest first. desc: newest first. One of: `asc`, `desc`. Default asc. | | `expand` | query | string | no | receipts: attach delivery and read times to outbound messages. One of: `receipts`. | | `limit` | query | integer | no | Page size, 1 to 200. Default 100. Default 100. | | `cursor` | query | string | no | The next_cursor from the previous page. Treat it as opaque. | **Response 200**: OK (schema `MessageList`) | Field | Type | Description | | --- | --- | --- | | `phone` | string | | | `data` | array of Message | | | `data[][].id` | integer | Internal row id (used by cursors). | | `data[][].message_id` | string, nullable | WhatsApp message id (wamid). | | `data[][].phone` | string | Contact phone number. | | `data[][].sent_at` | string (date-time) | When the message was sent or received. | | `data[][].sender_type` | string | customer: the contact. bot: the AI agent. agent: a team member (recorded on messages sent from InboxWA). system: any other outbound message (automation, campaign, template, or an older message with no recorded author). A bot reply is never reported as a human. One of: `customer`, `bot`, `agent`, `system`. | | `data[][].actor` | object, nullable | | | `data[][].sender_label` | string, nullable | Display label recorded at send time. | | `data[][].type` | string, nullable | text, template, image, audio, document and so on. | | `data[][].text` | string, nullable | Message text. | | `data[][].template_name` | string, nullable | Template name when a template was sent. | | `data[][].media_url` | string, nullable | Media location, if any. | | `data[][].status` | string, nullable | Inbound: unread or read. Outbound: sent, delivered, read or failed. | | `data[][].source` | string, nullable | Channel, normally whatsapp. | | `data[][].receipts` | array of Receipt | Only with expand=receipts, and only on outbound messages. | | `data[][].receipts[][].status` | string | One of: `sent`, `delivered`, `read`, `failed`. | | `data[][].receipts[][].at` | string (date-time) | When WhatsApp reported it. | | `data[][].receipts[][].error` | string | Failure reason. | | `next_cursor` | string, nullable | | Example: ```json { "phone": "919876543210", "data": [ { "id": 1041, "message_id": "wamid.HBgM...C1", "sent_at": "2026-10-02T09:00:00.000Z", "sender_type": "customer", "actor": null, "sender_label": null, "type": "text", "text": "My server is down", "template_name": null, "media_url": null, "status": "read", "source": "whatsapp" }, { "id": 1044, "message_id": "wamid.HBgM...A1", "sent_at": "2026-10-02T09:05:12.000Z", "sender_type": "agent", "actor": { "user_id": 41, "name": "Anita" }, "sender_label": "Anita", "type": "text", "text": "We are looking into it.", "template_name": null, "media_url": null, "status": "read", "source": "whatsapp", "receipts": [ { "status": "sent", "at": "2026-10-02T09:05:12.000Z" }, { "status": "delivered", "at": "2026-10-02T09:05:14.000Z" }, { "status": "read", "at": "2026-10-02T09:05:50.000Z" } ] } ], "next_cursor": null } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_order` | order is not asc or desc. | (Also at https://inboxwa.online/docs/api/op/listMessages.md) #### Receipts for a conversation `GET /api/reporting/v1/conversations/{phone}/receipts` (operationId: `conversationReceipts`, group: Reporting) Delivery and read receipts for the outbound messages in a chat, most recent first, in one call. Receipts exist from the day recording began; earlier messages show only their latest status. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | | `since` | query | string (date-time) | no | Only messages at or after this time. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `limit` | query | integer | no | Page size, 1 to 200. Default 100. Default 100. | **Response 200**: OK (schema `ConversationReceipts`) | Field | Type | Description | | --- | --- | --- | | `phone` | string | | | `data` | array of object | | | `data[][].message_id` | string | | | `data[][].sent_at` | string (date-time) | | | `data[][].sender_type` | string | customer: the contact. bot: the AI agent. agent: a team member (recorded on messages sent from InboxWA). system: any other outbound message (automation, campaign, template, or an older message with no recorded author). A bot reply is never reported as a human. One of: `customer`, `bot`, `agent`, `system`. | | `data[][].current_status` | string, nullable | | | `data[][].receipts` | array of Receipt | | | `data[][].receipts[][].status` | string | One of: `sent`, `delivered`, `read`, `failed`. | | `data[][].receipts[][].at` | string (date-time) | When WhatsApp reported it. | | `data[][].receipts[][].error` | string | Failure reason. | Example: ```json { "phone": "919876543210", "data": [ { "message_id": "wamid.HBgM...A1", "sent_at": "2026-10-02T09:05:12.000Z", "sender_type": "agent", "current_status": "read", "receipts": [ { "status": "sent", "at": "2026-10-02T09:05:12.000Z" }, { "status": "delivered", "at": "2026-10-02T09:05:14.000Z" }, { "status": "read", "at": "2026-10-02T09:05:50.000Z" } ] } ] } ``` (Also at https://inboxwa.online/docs/api/op/conversationReceipts.md) #### Who has opened a chat `GET /api/reporting/v1/conversations/{phone}/seen-by` (operationId: `seenBy`, group: Reporting) Which team members opened this chat in the inbox or the embedded panel, when first and last, and how many customer messages each has not yet seen. "Opened" means opened in InboxWA, not read on a phone. Recording began on the date in recorded_from. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: OK (schema `SeenBy`) | Field | Type | Description | | --- | --- | --- | | `phone` | string | | | `last_customer_message_at` | string (date-time), nullable | | | `seen_by_anyone` | boolean | Some team member opened the chat after the last customer message. | | `seen_by` | array of object | | | `seen_by[][].user_id` | integer | | | `seen_by[][].name` | string, nullable | | | `seen_by[][].first_opened_at` | string (date-time) | | | `seen_by[][].last_opened_at` | string (date-time) | | | `seen_by[][].opens` | integer | Number of recorded opens. | | `seen_by[][].unseen_customer_messages` | integer | Customer messages that arrived after this member last opened it. | | `recorded_from` | string (date-time), nullable | When the workspace began recording opens. Nothing earlier can be reported. | Example: ```json { "phone": "919876543210", "last_customer_message_at": "2026-10-02T09:06:40.000Z", "seen_by_anyone": true, "seen_by": [ { "user_id": 41, "name": "Anita", "first_opened_at": "2026-10-02T09:04:30.000Z", "last_opened_at": "2026-10-02T09:05:20.000Z", "opens": 2, "unseen_customer_messages": 1 }, { "user_id": 52, "name": "Vikram", "first_opened_at": "2026-10-02T09:08:11.000Z", "last_opened_at": "2026-10-02T09:08:11.000Z", "opens": 1, "unseen_customer_messages": 0 } ], "recorded_from": "2026-09-30T04:00:00.000Z" } ``` (Also at https://inboxwa.online/docs/api/op/seenBy.md) #### Open log for a chat `GET /api/reporting/v1/conversations/{phone}/views` (operationId: `listViews`, group: Reporting) Every recorded open of this chat, newest first: who opened it, when, and how many unread customer messages that open cleared. Repeated opens by the same member within a minute count once. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | | `limit` | query | integer | no | Page size, 1 to 200. Default 100. Default 100. | | `cursor` | query | string | no | The next_cursor from the previous page. Treat it as opaque. | **Response 200**: OK (schema `Views`) | Field | Type | Description | | --- | --- | --- | | `phone` | string | | | `data` | array of object | | | `data[][].user` | object, nullable | | | `data[][].opened_at` | string (date-time) | | | `data[][].marked_read` | integer | Unread customer messages this open cleared. | | `next_cursor` | string, nullable | | Example: ```json { "phone": "919876543210", "data": [ { "user": { "user_id": 52, "name": "Vikram" }, "opened_at": "2026-10-02T09:08:11.000Z", "marked_read": 1 }, { "user": { "user_id": 41, "name": "Anita" }, "opened_at": "2026-10-02T09:04:30.000Z", "marked_read": 2 } ], "next_cursor": null } ``` (Also at https://inboxwa.online/docs/api/op/listViews.md) #### Replies in a conversation `GET /api/reporting/v1/conversations/{phone}/replies` (operationId: `conversationReplies`, group: Reporting) Every outbound message with who sent it and how long the customer had waited. A bot usually answers in seconds, so there are two clocks: a bot or system reply is timed from the moment the customer began waiting; an agent reply from the moment the customer began waiting for a person. first_human_response is the number to hold a team to. A send that failed never counts as a reply. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | | `since` | query | string (date-time) | no | Only messages at or after this time. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | **Response 200**: OK (schema `ConversationReplies`) | Field | Type | Description | | --- | --- | --- | | `phone` | string | | | `first_customer_message_at` | string (date-time), nullable | | | `replies` | array of object | | | `replies[][].message_id` | string, nullable | | | `replies[][].sent_at` | string (date-time) | | | `replies[][].sender_type` | string | customer: the contact. bot: the AI agent. agent: a team member (recorded on messages sent from InboxWA). system: any other outbound message (automation, campaign, template, or an older message with no recorded author). A bot reply is never reported as a human. One of: `customer`, `bot`, `agent`, `system`. | | `replies[][].actor` | object, nullable | | | `replies[][].sender_label` | string, nullable | | | `replies[][].answers_message_id` | string, nullable | The customer message this reply answered. | | `replies[][].status` | string, nullable | | | `replies[][].response_seconds` | integer, nullable | Bot and system replies: seconds since the customer began waiting. Agent replies: seconds since the customer began waiting for a person. Null when nobody was waiting, or the send failed. | | `first_response` | object, nullable | | | `first_human_response` | object, nullable | | Example: ```json { "phone": "919876543210", "first_customer_message_at": "2026-10-02T09:00:00.000Z", "replies": [ { "message_id": "wamid.HBgM...B1", "sent_at": "2026-10-02T09:00:04.000Z", "sender_type": "bot", "actor": null, "sender_label": null, "answers_message_id": "wamid.HBgM...C1", "status": "delivered", "response_seconds": 4 }, { "message_id": "wamid.HBgM...A1", "sent_at": "2026-10-02T09:05:12.000Z", "sender_type": "agent", "actor": { "user_id": 41, "name": "Anita" }, "sender_label": "Anita", "answers_message_id": "wamid.HBgM...C1", "status": "read", "response_seconds": 312 } ], "first_response": { "message_id": "wamid.HBgM...B1", "sender_type": "bot", "actor": null, "response_seconds": 4 }, "first_human_response": { "message_id": "wamid.HBgM...A1", "sender_type": "agent", "actor": { "user_id": 41, "name": "Anita" }, "response_seconds": 312 } } ``` (Also at https://inboxwa.online/docs/api/op/conversationReplies.md) #### Who has held a chat `GET /api/reporting/v1/conversations/{phone}/assignment-history` (operationId: `assignmentHistory`, group: Reporting) Claims, releases, transfers and resolutions, newest first. From detailed_since the record is exact; before that it is rebuilt from the team audit log, which has claims, releases and transfers but not resolutions (source: audit). Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `phone` | path | string | yes | Contact phone number with the country code, digits only. A leading + is accepted. A 10-digit number with no country code is treated as Indian (91 is added). | **Response 200**: OK (schema `AssignmentHistory`) | Field | Type | Description | | --- | --- | --- | | `phone` | string | | | `data` | array of object | | | `data[][].event` | string | One of: `claimed`, `released`, `transferred`, `resolved`. | | `data[][].at` | string (date-time) | | | `data[][].from` | object, nullable | | | `data[][].to` | object, nullable | | | `data[][].by` | object, nullable | | | `data[][].reason` | string, nullable | | | `data[][].source` | string | events: recorded exactly. audit: reconstructed from the earlier team audit log (claims, releases, transfers only). One of: `events`, `audit`. | | `detailed_since` | string (date-time), nullable | From here on, history is exact; before it, only the audit log is available. | Example: ```json { "phone": "919876543210", "data": [ { "event": "transferred", "at": "2026-10-02T09:40:11.000Z", "from": { "user_id": 41, "name": "Anita" }, "to": { "user_id": 52, "name": "Vikram" }, "by": { "user_id": 41, "name": "Anita" }, "reason": "handoff_transfer", "source": "events" }, { "event": "claimed", "at": "2026-10-02T09:04:30.000Z", "from": null, "to": { "user_id": 41, "name": "Anita" }, "by": { "user_id": 41, "name": "Anita" }, "reason": "human_claimed", "source": "events" } ], "detailed_since": "2026-09-30T04:00:00.000Z" } ``` (Also at https://inboxwa.online/docs/api/op/assignmentHistory.md) #### Messages by delivery status `GET /api/reporting/v1/messages` (operationId: `listMessagesByStatus`, group: Reporting) Outbound messages currently in a status, newest first. status=failed is the delivery-problem report: each row carries the error text recorded at send time. The window defaults to 7 days, up to 90. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | string | yes | Required. The delivery status to list. One of: `sent`, `delivered`, `read`, `failed`, `pending`. | | `since` | query | string (date-time) | no | Start of the window: an ISO 8601 timestamp or a date (YYYY-MM-DD, start of that UTC day). Default: 7 days ago. Alias: from. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `phone` | query | string | no | Only this contact. | | `limit` | query | integer | no | Page size, 1 to 200. Default 50. Default 50. | | `cursor` | query | string | no | The next_cursor from the previous page. Treat it as opaque. | **Response 200**: OK (schema `FailedMessageList`) | Field | Type | Description | | --- | --- | --- | | `data` | array of object | | | `data[][].id` | integer | | | `data[][].message_id` | string, nullable | | | `data[][].phone` | string | | | `data[][].sent_at` | string (date-time) | | | `data[][].sender_type` | string | customer: the contact. bot: the AI agent. agent: a team member (recorded on messages sent from InboxWA). system: any other outbound message (automation, campaign, template, or an older message with no recorded author). A bot reply is never reported as a human. One of: `customer`, `bot`, `agent`, `system`. | | `data[][].actor` | object, nullable | | | `data[][].sender_label` | string, nullable | | | `data[][].type` | string, nullable | | | `data[][].template_name` | string, nullable | | | `data[][].media_url` | string, nullable | | | `data[][].status` | string, nullable | | | `data[][].source` | string, nullable | | | `data[][].error` | string, nullable | Error text recorded at send time. | | `next_cursor` | string, nullable | | | `window` | Window | | | `window.from` | string (date-time) | Start of the window. | | `window.to` | string (date-time) | End of the window. | Example: ```json { "data": [ { "id": 1102, "message_id": "wamid.HBgM...F1", "phone": "919876543210", "sent_at": "2026-10-02T08:12:00.000Z", "sender_type": "system", "actor": null, "sender_label": null, "type": "template", "template_name": "renewal_reminder", "media_url": null, "status": "failed", "source": null, "error": "#131047 Re-engagement message" } ], "next_cursor": null, "window": { "from": "2026-09-25T09:30:00.000Z", "to": "2026-10-02T09:30:00.000Z" } } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_status` | status is missing or not recognised. | | 400 | `window_too_large` | The window is longer than 90 days. | (Also at https://inboxwa.online/docs/api/op/listMessagesByStatus.md) #### Get a message `GET /api/reporting/v1/messages/{id}` (operationId: `getMessage`, group: Reporting) One message by its WhatsApp message id (wamid), with its current delivery status and, for a failed send, the reason. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | WhatsApp message id. | **Response 200**: OK (schema `MessageDetail`) Example: ```json { "id": 1044, "message_id": "wamid.HBgM...A1", "sent_at": "2026-10-02T09:05:12.000Z", "sender_type": "agent", "actor": { "user_id": 41, "name": "Anita" }, "sender_label": "Anita", "type": "text", "text": "We are looking into it.", "template_name": null, "media_url": null, "status": "read", "source": "whatsapp", "phone": "919876543210", "error": null } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 404 | `not_found` | No such message in this workspace. | (Also at https://inboxwa.online/docs/api/op/getMessage.md) #### Delivery and read receipts `GET /api/reporting/v1/messages/{id}/receipts` (operationId: `messageReceipts`, group: Reporting) Every status change of one message with the time WhatsApp reported it: sent, delivered, read. A read receipt arrives only if the customer has read receipts switched on; WhatsApp reports nothing otherwise. Receipts exist from recorded_from; messages before that keep only their latest status. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `contacts`, `inbox:read`, `contacts:read`. Chat content: messages, names and phone numbers. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | yes | WhatsApp message id. | **Response 200**: OK (schema `MessageReceipts`) | Field | Type | Description | | --- | --- | --- | | `message_id` | string | | | `phone` | string, nullable | | | `current_status` | string, nullable | | | `receipts` | array of Receipt | | | `receipts[][].status` | string | One of: `sent`, `delivered`, `read`, `failed`. | | `receipts[][].at` | string (date-time) | When WhatsApp reported it. | | `receipts[][].error` | string | Failure reason. | | `recorded_from` | string (date-time), nullable | When receipts began being recorded for this message. Earlier chats keep only the latest status. | Example: ```json { "message_id": "wamid.HBgM...A1", "phone": "919876543210", "current_status": "read", "receipts": [ { "status": "sent", "at": "2026-10-02T09:05:12.000Z" }, { "status": "delivered", "at": "2026-10-02T09:05:14.000Z" }, { "status": "read", "at": "2026-10-02T09:05:50.000Z" } ], "recorded_from": "2026-10-02T09:05:12.000Z" } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 404 | `not_found` | No such message in this workspace. | (Also at https://inboxwa.online/docs/api/op/messageReceipts.md) #### Replies across the workspace `GET /api/reporting/v1/replies` (operationId: `listReplies`, group: Reporting) Outbound messages in the window, newest first, filterable by team member and sender type, each with its response time. Use it to audit one person, or to see what the bot answered. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `since` | query | string (date-time) | no | Start of the window: an ISO 8601 timestamp or a date (YYYY-MM-DD, start of that UTC day). Default: 7 days ago. Alias: from. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `agent_id` | query | integer | no | Only replies sent by this team member. | | `sender_type` | query | string | no | Only this kind of sender. One of: `agent`, `bot`, `system`. | | `phone` | query | string | no | Only this contact. | | `limit` | query | integer | no | Page size, 1 to 200. Default 50. Default 50. | | `cursor` | query | string | no | The next_cursor from the previous page. Treat it as opaque. | **Response 200**: OK (schema `ReplyList`) | Field | Type | Description | | --- | --- | --- | | `data` | array of object | | | `data[][].message_id` | string, nullable | | | `data[][].phone` | string | | | `data[][].sent_at` | string (date-time) | | | `data[][].sender_type` | string | customer: the contact. bot: the AI agent. agent: a team member (recorded on messages sent from InboxWA). system: any other outbound message (automation, campaign, template, or an older message with no recorded author). A bot reply is never reported as a human. One of: `customer`, `bot`, `agent`, `system`. | | `data[][].actor` | object, nullable | | | `data[][].sender_label` | string, nullable | | | `data[][].status` | string, nullable | | | `data[][].response_seconds` | integer, nullable | | | `next_cursor` | string, nullable | | | `window` | Window | | | `window.from` | string (date-time) | Start of the window. | | `window.to` | string (date-time) | End of the window. | Example: ```json { "data": [ { "message_id": "wamid.HBgM...A1", "phone": "919876543210", "sent_at": "2026-10-02T09:05:12.000Z", "sender_type": "agent", "actor": { "user_id": 41, "name": "Anita" }, "sender_label": "Anita", "status": "read", "response_seconds": 312 } ], "next_cursor": null, "window": { "from": "2026-09-25T09:30:00.000Z", "to": "2026-10-02T09:30:00.000Z" } } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_agent_id` | agent_id is not a user id. | | 400 | `invalid_sender_type` | sender_type is not agent, bot or system. | (Also at https://inboxwa.online/docs/api/op/listReplies.md) #### First response times `GET /api/reporting/v1/metrics/first-response` (operationId: `firstResponseMetrics`, group: Reporting) How long customers waited for a first reply, and for a first human reply, across the workspace: count, median, 90th percentile and mean. An episode is a wait that begins with the first customer message in a chat or the first after any outbound message. Group by day (in a time zone you choose) or by the team member who gave the first human reply. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `since` | query | string (date-time) | no | Start of the window: an ISO 8601 timestamp or a date (YYYY-MM-DD, start of that UTC day). Default: 7 days ago. Alias: from. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `group_by` | query | string | no | Break the result down. One of: `none`, `day`, `agent`. Default none. | | `tz` | query | string | no | IANA time zone for day grouping. Default UTC. | **Response 200**: OK (schema `FirstResponseMetrics`) | Field | Type | Description | | --- | --- | --- | | `window` | Window | | | `window.from` | string (date-time) | Start of the window. | | `window.to` | string (date-time) | End of the window. | | `time_zone` | string | Time zone used for day grouping. | | `episodes` | integer | Waits that began in the window: the first customer message in a chat, or the first after any outbound message. | | `unanswered_episodes` | integer | Episodes with no delivered reply. | | `first_response` | object | | | `first_response.count` | integer | Number of episodes measured. | | `first_response.median_seconds` | integer, nullable | Median, seconds. | | `first_response.p90_seconds` | integer, nullable | 90th percentile, seconds. | | `first_response.mean_seconds` | integer, nullable | Mean, seconds. | | `first_human_response` | object | | | `first_human_response.count` | integer | Number of episodes measured. | | `first_human_response.median_seconds` | integer, nullable | Median, seconds. | | `first_human_response.p90_seconds` | integer, nullable | 90th percentile, seconds. | | `first_human_response.mean_seconds` | integer, nullable | Mean, seconds. | | `truncated` | boolean | True when more than 100,000 episodes were in the window; narrow it. | | `groups` | array of object | Present when group_by is day or agent. | | `groups[][].day` | string | YYYY-MM-DD in time_zone (group_by=day). | | `groups[][].episodes` | integer | | | `groups[][].agent` | MemberRef | | | `groups[][].agent.user_id` | integer | Team member user id. | | `groups[][].agent.name` | string, nullable | Display name. | | `groups[][].first_response` | object | | | `groups[][].first_response.count` | integer | Number of episodes measured. | | `groups[][].first_response.median_seconds` | integer, nullable | Median, seconds. | | `groups[][].first_response.p90_seconds` | integer, nullable | 90th percentile, seconds. | | `groups[][].first_response.mean_seconds` | integer, nullable | Mean, seconds. | | `groups[][].first_human_response` | object | | | `groups[][].first_human_response.count` | integer | Number of episodes measured. | | `groups[][].first_human_response.median_seconds` | integer, nullable | Median, seconds. | | `groups[][].first_human_response.p90_seconds` | integer, nullable | 90th percentile, seconds. | | `groups[][].first_human_response.mean_seconds` | integer, nullable | Mean, seconds. | Example: ```json { "window": { "from": "2026-09-25T09:30:00.000Z", "to": "2026-10-02T09:30:00.000Z" }, "time_zone": "Asia/Kolkata", "episodes": 5, "unanswered_episodes": 2, "first_response": { "count": 3, "median_seconds": 100, "p90_seconds": 241, "mean_seconds": 121 }, "first_human_response": { "count": 3, "median_seconds": 252, "p90_seconds": 304, "mean_seconds": 221 }, "truncated": false, "groups": [ { "agent": { "user_id": 41, "name": "Anita" }, "first_human_response": { "count": 2, "median_seconds": 282, "p90_seconds": 309, "mean_seconds": 282 } } ] } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_group_by` | group_by is not none, day or agent. | | 400 | `invalid_tz` | tz is not an IANA time zone. | | 403 | `workspace_metrics_restricted` | The caller is limited to assigned chats, so workspace-wide metrics are not available. | (Also at https://inboxwa.online/docs/api/op/firstResponseMetrics.md) #### Chats a team member has not opened `GET /api/reporting/v1/team/{userId}/unseen` (operationId: `unseenForMember`, group: Reporting) Chats assigned to this team member that have customer messages the member has not opened since they arrived, most recent first. ever_opened_by_user is false for chats the member has never opened. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `analytics`, `inbox:read`, `team_management:read`, `analytics:read`. Team accountability data: who opened, who replied, how fast. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `userId` | path | integer | yes | Team member user id. | | `since` | query | string (date-time) | no | Start of the window: an ISO 8601 timestamp or a date (YYYY-MM-DD, start of that UTC day). Default: 30 days ago. Alias: from. | | `to` | query | string (date-time) | no | End of the window (ISO 8601 or date). Default: now. Alias: until. | | `limit` | query | integer | no | Page size, 1 to 200. Default 50. Default 50. | **Response 200**: OK (schema `Unseen`) | Field | Type | Description | | --- | --- | --- | | `user` | object, nullable | | | `window` | Window | | | `window.from` | string (date-time) | Start of the window. | | `window.to` | string (date-time) | End of the window. | | `data` | array of object | | | `data[][].phone` | string | | | `data[][].contact_name` | string, nullable | | | `data[][].unseen_customer_messages` | integer | | | `data[][].last_customer_message_at` | string (date-time), nullable | | | `data[][].last_opened_at` | string (date-time), nullable | | | `data[][].ever_opened_by_user` | boolean | | Example: ```json { "user": { "user_id": 41, "name": "Anita" }, "window": { "from": "2026-09-02T00:00:00.000Z", "to": "2026-10-02T09:30:00.000Z" }, "data": [ { "phone": "919876543210", "contact_name": "Ravi Kumar", "unseen_customer_messages": 1, "last_customer_message_at": "2026-10-02T09:06:40.000Z", "last_opened_at": "2026-10-02T09:05:20.000Z", "ever_opened_by_user": true } ] } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_user_id` | userId is not a number. | | 404 | `not_found` | No such team member in this workspace. | (Also at https://inboxwa.online/docs/api/op/unseenForMember.md) ### Team Roster, performance, SLA and the hand-off queue. Part of the full API (Scale plan). The API is the same surface the web app uses, over 500 routes; GET /api/docs/routes lists every one. #### Team roster `GET /api/team/v2/roster` (operationId: `teamRoster`, group: Team) Active team members with working hours, capacity and skills. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `team_management`, `team_management:read`. **Response 200**: The roster. Example: ```json { "members": [ { "user_id": 41, "display_name": "Anita", "role": "agent" } ] } ``` (Also at https://inboxwa.online/docs/api/op/teamRoster.md) #### Team performance `GET /api/team/v2/performance` (operationId: `teamPerformance`, group: Team) Per team member: messages sent, chats replied to, active chats and pickup time over a range. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `team_management`, `team_management:read`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `range` | query | string | no | Window, for example 24h, 7d, 30d. Default 7d. Default 7d. | **Response 200**: Per-member figures. Example: ```json { "range": "7d", "members": [] } ``` (Also at https://inboxwa.online/docs/api/op/teamPerformance.md) #### SLA dashboard `GET /api/team/v2/sla/dashboard` (operationId: `slaDashboard`, group: Team) Live breach count, period totals, compliance percentage, a per-member leaderboard and recent breaches. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `team_management`, `team_management:read`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `range` | query | string | no | Window. Default 7d. Default 7d. | **Response 200**: SLA figures. Example: ```json { "range": "7d" } ``` (Also at https://inboxwa.online/docs/api/op/slaDashboard.md) #### Hand-off queue `GET /api/team/v2/handoff` (operationId: `handoffQueue`, group: Team) Chats waiting for a person. Claim, release and transfer are POST /api/team/v2/handoff/{phone}/claim, /release and /transfer. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `inbox`, `team_management`, `inbox:read`, `team_management:read`. **Response 200**: The queue. Example: ```json { "queue": [] } ``` (Also at https://inboxwa.online/docs/api/op/handoffQueue.md) ### Webhooks Signed events pushed to your endpoint, and the tools to configure and test them. Webhooks are part of Growth (five core events) and Scale (all events); Starter has none. Calling the configuration endpoints with a key is part of the full API (Scale plan); the dashboard does the same for Growth. #### Get webhook settings `GET /api/webhook/outbound/config` (operationId: `getWebhookConfig`, group: Webhooks) The saved endpoint and subscribed events, plus the catalogue of every event with whether this workspace can subscribe to it. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `settings`, `settings:read`. **Response 200**: OK (schema `WebhookConfig`) | Field | Type | Description | | --- | --- | --- | | `configured` | boolean | | | `endpoint_url` | string | | | `events` | array of string | | | `enabled` | boolean | | | `updated_at` | string (date-time), nullable | | | `updated_by_name` | string, nullable | | | `valid_events` | array of string | | | `api_tier` | string | What the plan includes: none (Starter), core (Growth), full (Scale). One of: `none`, `core`, `full`. | | `webhooks_available` | boolean | False on Starter, which has no webhooks. | | `event_catalog` | array of object | | | `event_catalog[][].event` | string | | | `event_catalog[][].group` | string | | | `event_catalog[][].plan` | string | One of: `any`, `scale`. | | `event_catalog[][].description` | string | | | `event_catalog[][].available` | boolean | False when the event needs a plan this workspace is not on: Starter has none, Growth has the events marked any, Scale has all. | | `credit_alert_levels` | array of integer | | Example: ```json { "configured": true, "endpoint_url": "https://example.com/hooks/inboxwa", "events": [ "message_received", "message_status" ], "enabled": true, "updated_at": "2026-09-25T10:00:00.000Z", "updated_by_name": "admin@example.com", "valid_events": [ "message_received", "message_status" ], "api_tier": "full", "webhooks_available": true, "event_catalog": [ { "event": "message_status", "group": "Messages", "plan": "scale", "description": "An outbound message is delivered, read or fails, with the WhatsApp timestamp.", "available": true } ], "credit_alert_levels": [ 80, 100 ] } ``` (Also at https://inboxwa.online/docs/api/op/getWebhookConfig.md) #### Save webhook settings `PUT /api/webhook/outbound/config` (operationId: `putWebhookConfig`, group: Webhooks) Registers one HTTPS endpoint and the events to send. The signing secret is returned on the first save only. Scale events are refused for a workspace without API access, so nothing is subscribed that would never fire. Changes apply immediately. Part of the full API, which is included in the Scale plan. Workspace admin: a dashboard session, or a full-access key created by an admin. A scoped key is refused with scope_denied. **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `endpoint_url` | string (uri) | yes | http or https endpoint that accepts POST. | | `events` | array of string | yes | One or more event names from the catalogue. | | `enabled` | boolean | no | Default true. | | `credit_alert_levels` | array of integer | no | Percent levels for credits_threshold. Default [80, 100]. | Example: ```json { "endpoint_url": "https://example.com/hooks/inboxwa", "events": [ "message_received", "message_status", "credits_threshold" ], "credit_alert_levels": [ 80, 100 ] } ``` **Response 200**: Saved. Example: ```json { "success": true, "endpoint_url": "https://example.com/hooks/inboxwa", "events": [ "message_received", "message_status" ], "enabled": true, "signing_secret": "9f2c...e41a", "message": "Outbound webhook configured. Save the signing secret -- it will not be shown again." } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `endpoint_url_required` | No endpoint. | | 400 | `invalid_url` | Not an http or https URL. | | 400 | `events_required` | No events. | | 403 | `event_requires_scale` | A chosen event needs API access on the plan. | (Also at https://inboxwa.online/docs/api/op/putWebhookConfig.md) #### Rotate the signing secret `POST /api/webhook/outbound/rotate-secret` (operationId: `rotateWebhookSecret`, group: Webhooks) Replaces the signing secret and returns the new one once. Update your receiver before the next delivery. Part of the full API, which is included in the Scale plan. Workspace admin: a dashboard session, or a full-access key created by an admin. A scoped key is refused with scope_denied. **Response 200**: Rotated. Example: ```json { "success": true, "signing_secret": "41ab...07cd", "message": "Signing secret rotated. Save it now; it will not be shown again." } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 404 | `not_configured` | No webhook is configured. | (Also at https://inboxwa.online/docs/api/op/rotateWebhookSecret.md) #### Send a test event `POST /api/webhook/outbound/test` (operationId: `testWebhook`, group: Webhooks) Sends one signed event to the saved endpoint now and reports what it answered. The payload is the catalogue example, marked with test: true. It ignores your subscriptions and plan, so it checks connectivity and signature verification, not entitlement. Part of the full API, which is included in the Scale plan. Workspace admin: a dashboard session, or a full-access key created by an admin. A scoped key is refused with scope_denied. **Request body** (JSON) | Field | Type | Required | Description | | --- | --- | --- | --- | | `event` | string | no | A catalogue event. Default message_received. | Example: ```json { "event": "message_status" } ``` **Response 200**: OK (schema `WebhookTestResult`) | Field | Type | Description | | --- | --- | --- | | `ok` | boolean | | | `status` | integer, nullable | HTTP status your endpoint returned. | | `attempts` | integer | | | `error` | string, nullable | | | `duration_ms` | integer | | | `event` | string | | Example: ```json { "ok": true, "status": 200, "attempts": 1, "error": null, "duration_ms": 184, "event": "message_status" } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `unknown_event` | Not a catalogue event. | | 404 | `not_configured` | Save an endpoint first. | (Also at https://inboxwa.online/docs/api/op/testWebhook.md) #### Delivery log `GET /api/webhook/outbound/deliveries` (operationId: `webhookDeliveries`, group: Webhooks) The most recent deliveries with their outcome, so "is my endpoint actually receiving events" has an answer. Part of the full API, which is included in the Scale plan. Any member of the workspace. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size, 1 to 50. Default 15. Default 15. | **Response 200**: Deliveries, newest first. Example: ```json { "success": true, "deliveries": [ { "event_type": "message_status", "status": "success", "summary": "message_status -> https://example.com/hooks/inboxwa", "error_message": null, "created_at": "2026-10-02T09:22:49.000Z" } ] } ``` (Also at https://inboxwa.online/docs/api/op/webhookDeliveries.md) ### API keys Create, scope, limit, rotate and audit keys. Key management needs a workspace admin: a dashboard session, or a full-access key created by an admin. Keys are part of Growth and Scale; Starter has none. Managing keys with a key is part of the full API (Scale plan). #### List keys `GET /api/api-keys` (operationId: `listApiKeys`, group: API keys) Every key in the workspace with its scopes, expiry, rate limit and last use. The plaintext is never returned again after creation. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `settings`, `settings:read`. Read access for members with the settings permission. **Response 200**: OK (schema `ApiKeyList`) | Field | Type | Description | | --- | --- | --- | | `keys` | array of ApiKey | | | `keys[][].id` | integer | | | `keys[][].label` | string | | | `keys[][].last_4` | string | Last four characters, for recognising a key. | | `keys[][].created_at` | string (date-time) | | | `keys[][].last_used_at` | string (date-time), nullable | | | `keys[][].revoked_at` | string (date-time), nullable | | | `keys[][].scopes` | array of string | | | `keys[][].read_only` | boolean | | | `keys[][].expires_at` | string (date-time), nullable | | | `keys[][].expired` | boolean | | | `keys[][].rate_limit_per_min` | integer, nullable | Per-key override. | | `keys[][].effective_rate_limit_per_min` | integer, nullable | What actually applies. Null means unlimited. | | `keys[][].rotated_from` | integer, nullable | | | `keys[][].created_by_name` | string, nullable | | | `keys[][].revoked_by_name` | string, nullable | | | `defaults` | object | | | `defaults.rate_limit_per_min` | integer | | | `defaults.max_keys` | integer | | | `api_access` | boolean | | | `api_tier` | string | What the plan includes: none (Starter), core (Growth), full (Scale). One of: `none`, `core`, `full`. | Example: ```json { "keys": [ { "id": 7, "label": "Dashboard", "last_4": "x9Kq", "created_at": "2026-09-25T10:00:00.000Z", "last_used_at": "2026-10-02T09:29:00.000Z", "revoked_at": null, "scopes": [ "inbox:read", "team_management:read" ], "read_only": true, "expires_at": "2026-12-24T10:00:00.000Z", "expired": false, "rate_limit_per_min": 120, "effective_rate_limit_per_min": 120, "rotated_from": null, "created_by_name": "admin@example.com", "revoked_by_name": null } ], "defaults": { "rate_limit_per_min": 600, "max_keys": 20 }, "api_access": true, "api_tier": "full" } ``` (Also at https://inboxwa.online/docs/api/op/listApiKeys.md) #### Create a key `POST /api/api-keys` (operationId: `createApiKey`, group: API keys) Mints a key. The plaintext is in the response and cannot be shown again. Only workspace admins can create keys: in the dashboard, or with a full-access key created by an admin. Scopes may include the :read variants, which make a key that can call GET endpoints and can never change anything. Part of the full API, which is included in the Scale plan. Workspace admin: a dashboard session, or a full-access key created by an admin. A scoped key is refused with scope_denied. **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | string | yes | A name to recognise the key by. | | `scopes` | array of string | no | all, or any of the scopes from GET /api/api-keys/scopes. Default: all. | | `expires_in_days` | integer | no | Expire after this many days. Or use expires_at. Range 1..730. | | `expires_at` | string (date-time) | no | A future expiry, within 730 days. | | `rate_limit_per_min` | integer | no | Per-key requests per minute. Omit for the workspace default. Range 10..100000. | Example: ```json { "label": "Dashboard", "scopes": [ "inbox:read", "team_management:read" ], "expires_in_days": 90, "rate_limit_per_min": 120 } ``` **Response 200**: OK (schema `ApiKeyCreated`) | Field | Type | Description | | --- | --- | --- | | `id` | integer | | | `label` | string | | | `key` | string | The plaintext key. Shown once and never again. | | `last_4` | string | | | `scopes` | array of string | | | `read_only` | boolean | | | `expires_at` | string (date-time), nullable | | | `rate_limit_per_min` | integer, nullable | | | `created_at` | string (date-time) | | | `message` | string | | Example: ```json { "id": 7, "label": "Dashboard", "key": "iwk_2vN0k1...x9Kq", "last_4": "x9Kq", "scopes": [ "inbox:read", "team_management:read" ], "read_only": true, "expires_at": "2026-12-24T10:00:00.000Z", "rate_limit_per_min": 120, "created_at": "2026-09-25T10:00:00.000Z", "message": "Save this key now. It can’t be shown again." } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `label_required` | No label. | | 400 | `invalid_scopes` | No valid scope in the list. | | 400 | `invalid_expiry` | Expiry outside 1 to 730 days. | | 400 | `invalid_rate_limit` | Limit outside 10 to 100000. | | 400 | `too_many_keys` | The workspace already has 20 active keys. | (Also at https://inboxwa.online/docs/api/op/createApiKey.md) #### List scopes `GET /api/api-keys/scopes` (operationId: `apiKeyScopes`, group: API keys) The scopes a key can be limited to, grouped, with a description of each and the :read variant where one exists. The panel renders from this, so a new scope needs no client change. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `settings`, `settings:read`. **Response 200**: OK (schema `ScopeCatalog`) | Field | Type | Description | | --- | --- | --- | | `groups` | object | Group key to display name. | | `scopes` | array of object | | | `scopes[][].key` | string | | | `scopes[][].group` | string | | | `scopes[][].label` | string | | | `scopes[][].description` | string | | | `scopes[][].viewOnly` | boolean | Only ever grants viewing. | | `scopes[][].readScope` | string, nullable | The :read variant of this scope, when there is one. | | `readScopes` | array of string | | Example: ```json { "groups": { "workspace": "Workspace", "ai": "AI agent", "admin": "Admin and billing", "channel": "Channel" }, "scopes": [ { "key": "inbox", "group": "workspace", "label": "Inbox", "description": "Read chats and messages; claim, release, transfer, snooze, label and note conversations.", "readScope": "inbox:read" } ], "readScopes": [ "inbox:read", "contacts:read" ] } ``` (Also at https://inboxwa.online/docs/api/op/apiKeyScopes.md) #### Update a key `PATCH /api/api-keys/{id}` (operationId: `updateApiKey`, group: API keys) Rename a key, or change its expiry or rate limit. Send null for expires_at or rate_limit_per_min to clear them. Scopes cannot be changed: rotate the key instead, so an old secret can never quietly become more powerful. Part of the full API, which is included in the Scale plan. Workspace admin: a dashboard session, or a full-access key created by an admin. A scoped key is refused with scope_denied. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | integer | yes | Key id. | **Request body** (JSON, required) | Field | Type | Required | Description | | --- | --- | --- | --- | | `label` | string | no | | | `expires_in_days` | integer, nullable | no | | | `expires_at` | string (date-time), nullable | no | | | `rate_limit_per_min` | integer, nullable | no | | Example: ```json { "label": "Dashboard (prod)", "rate_limit_per_min": 300 } ``` **Response 200**: Updated. Example: ```json { "success": true } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `nothing_to_update` | The body changed nothing. | | 404 | `not_found` | No such key in this workspace. | | 400 | `already_revoked` | The key is revoked. | (Also at https://inboxwa.online/docs/api/op/updateApiKey.md) #### Revoke a key `DELETE /api/api-keys/{id}` (operationId: `revokeApiKey`, group: API keys) Ends a key immediately. The row is kept so its audit trail survives. Part of the full API, which is included in the Scale plan. Workspace admin: a dashboard session, or a full-access key created by an admin. A scoped key is refused with scope_denied. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | integer | yes | Key id. | **Response 200**: Revoked. Example: ```json { "success": true } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 404 | `not_found` | No such key. | | 400 | `already_revoked` | Already revoked. | (Also at https://inboxwa.online/docs/api/op/revokeApiKey.md) #### Rotate a key `POST /api/api-keys/{id}/rotate` (operationId: `rotateApiKey`, group: API keys) Issues a replacement with the same scopes, limit and label, and ends the old key. Set grace_hours (up to 168) to keep the old key valid while a deployed integration switches over; 0 revokes it at once. The new plaintext is shown once. Part of the full API, which is included in the Scale plan. Workspace admin: a dashboard session, or a full-access key created by an admin. A scoped key is refused with scope_denied. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | integer | yes | Key id. | **Request body** (JSON) | Field | Type | Required | Description | | --- | --- | --- | --- | | `grace_hours` | integer | no | Range 0..168. | Example: ```json { "grace_hours": 24 } ``` **Response 200**: OK (schema `ApiKeyRotated`) | Field | Type | Description | | --- | --- | --- | | `id` | integer | | | `label` | string | | | `key` | string | The new plaintext key. Shown once. | | `last_4` | string | | | `scopes` | array of string | | | `rotated_from` | integer | | | `old_key_valid_until` | string (date-time), nullable | Set when grace_hours was used. | | `message` | string | | Example: ```json { "id": 8, "label": "Dashboard", "key": "iwk_9pQ2m8...t3Lw", "last_4": "t3Lw", "scopes": [ "inbox:read", "team_management:read" ], "rotated_from": 7, "old_key_valid_until": "2026-10-03T10:00:00.000Z", "message": "Save this key now. It can’t be shown again." } ``` **Errors specific to this call.** The common ones (401, 403, 429 and the rest) are in the [Errors guide](https://inboxwa.online/docs/api/guide/errors.md). | Status | error | When | | --- | --- | --- | | 400 | `invalid_grace` | grace_hours outside 0 to 168. | | 404 | `not_found` | No such key. | | 400 | `already_revoked` | The key is already revoked. | (Also at https://inboxwa.online/docs/api/op/rotateApiKey.md) #### Key activity `GET /api/api-keys/{id}/usage` (operationId: `apiKeyUsage`, group: API keys) Calls, errors, a daily series, top endpoints and the last 50 calls for one key. Refused and rate-limited calls are included. The log keeps 30 days. Part of the full API, which is included in the Scale plan. Any one of these scopes is enough: `settings`, `settings:read`. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | integer | yes | Key id. | | `range` | query | string | no | Window. One of: `7d`, `30d`. Default 7d. | **Response 200**: OK (schema `KeyUsage`) | Field | Type | Description | | --- | --- | --- | | `range` | string | | | `totals` | object | | | `totals.calls` | integer | | | `totals.errors` | integer | | | `totals.forbidden` | integer | | | `totals.serverErrors` | integer | | | `daily` | array of object | | | `daily[][].date` | string | | | `daily[][].calls` | integer | | | `daily[][].errors` | integer | | | `topEndpoints` | array of object | | | `topEndpoints[][].method` | string | | | `topEndpoints[][].path` | string | | | `topEndpoints[][].calls` | integer | | | `topEndpoints[][].errors` | integer | | | `recent` | array of object | | | `recent[][].id` | integer | | | `recent[][].ts` | string (date-time) | | | `recent[][].method` | string | | | `recent[][].path` | string | | | `recent[][].status` | integer | | | `recent[][].ip` | string, nullable | | Example: ```json { "range": "7d", "totals": { "calls": 2104, "errors": 3, "forbidden": 1, "serverErrors": 0 }, "daily": [ { "date": "2026-10-01", "calls": 310, "errors": 0 } ], "topEndpoints": [ { "method": "GET", "path": "/api/reporting/v1/conversations", "calls": 1800, "errors": 0 } ], "recent": [ { "id": 99021, "ts": "2026-10-02T09:29:10.000Z", "method": "GET", "path": "/api/reporting/v1/conversations", "status": 200, "ip": "203.0.113.7" } ] } ``` (Also at https://inboxwa.online/docs/api/op/apiKeyUsage.md) ### Credits AI credit balance and the ledger behind it. Part of the full API (Scale plan). #### Credit balance `GET /api/agent-hub/credits` (operationId: `getCredits`, group: Credits) The plan, the current calendar-month cycle (resets on the 1st, India time), credits used, the allocation (bundle plus active top-ups), remaining credits and any overage. Also returns usage by feature. Requires the AI agent to be enabled for the workspace. Part of the full API, which is included in the Scale plan. Any authenticated caller in a workspace with the AI agent enabled. **Response 200**: OK (schema `Credits`) | Field | Type | Description | | --- | --- | --- | | `plan` | object | | | `plan.key` | string | | | `plan.label` | string | | | `plan.monthly_inr` | number | | | `plan.credits_allocation` | integer | | | `plan.overage_inr_per_credit` | number | | | `cycle` | object | | | `cycle.start` | string (date-time) | | | `cycle.end` | string (date-time) | | | `cycle.kind` | string | calendar_month. | | `used_credits` | number | | | `allocation` | number | Plan bundle plus active top-ups. | | `topup_credits` | number | | | `remaining_credits` | number | | | `overage_credits` | number | | | `overage_inr` | number | | Example: ```json { "plan": { "key": "growth", "label": "Growth", "monthly_inr": 2499, "credits_allocation": 2500, "overage_inr_per_credit": 0.2 }, "cycle": { "start": "2026-09-30T18:30:00.000Z", "end": "2026-10-02T09:30:00.000Z", "kind": "calendar_month" }, "used_credits": 3120.4, "allocation": 12500, "topup_credits": 10000, "remaining_credits": 9379.6, "overage_credits": 0, "overage_inr": 0 } ``` (Also at https://inboxwa.online/docs/api/op/getCredits.md) #### Credit ledger `GET /api/agent-hub/credits/ledger` (operationId: `getCreditsLedger`, group: Credits) Every AI action this cycle with its feature, tokens and the credits it cost, newest first. One credit is 5,000 tokens. Part of the full API, which is included in the Scale plan. Any authenticated caller in a workspace with the AI agent enabled. **Parameters** | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size, 1 to 200. Default 50. Default 50. | | `offset` | query | integer | no | Rows to skip. Default 0. | | `feature` | query | string | no | Only this feature key. | | `channel` | query | string | no | Only this channel: whatsapp, website, workflows, marketing or internal. | **Response 200**: Ledger rows. Example: ```json { "events": [ { "id": 88123, "created_at": "2026-10-02T09:14:07.000Z", "feature": "chat_reply", "label": "AI chat reply", "chat_id": "919876543210", "detail": null, "input_tokens": 3200, "cached_tokens": 2600, "output_tokens": 180, "audio_seconds": 0, "credits": 0.29 } ], "total": 1, "limit": 50, "offset": 0 } ``` (Also at https://inboxwa.online/docs/api/op/getCreditsLedger.md) ## Webhook events Each event is a signed POST to your endpoint. See the Webhooks guide for the envelope, headers and signature verification. ### `message_received` A customer message arrives. Group: Messages. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `message_type` | text, image, audio, document and so on. | | `text` | Message text, truncated to 500 characters. | | `message_id` | WhatsApp message id (wamid). | | `userId` | Workspace user id the chat belongs to. | Example delivery: ```json { "event": "message_received", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "message_type": "text", "text": "Server is still down", "message_id": "wamid.HBgM...A1", "userId": 6 } } ``` ### `message_sent` A message leaves the workspace (team member, bot, workflow or campaign). Group: Messages. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `type` | text, template, image and so on. | | `text` | Message text, truncated to 500 characters. | | `template` | Template name when a template was sent, else null. | | `message_id` | WhatsApp message id (wamid). | | `status` | Status at send time, usually sent. | | `userId` | Workspace user id the chat belongs to. | Example delivery: ```json { "event": "message_sent", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "type": "text", "text": "We are looking into it.", "template": null, "message_id": "wamid.HBgM...B7", "status": "sent", "userId": 6 } } ``` ### `contact_created` A new contact is created. Group: Contacts. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `userId` | Workspace user id. | Example delivery: ```json { "event": "contact_created", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "userId": 6 } } ``` ### `handoff` A chat is handed to a human. Group: Conversations. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `note` | Handoff note, truncated to 500 characters. | | `priority` | normal or high. | | `userId` | Workspace user id. | Example delivery: ```json { "event": "handoff", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "note": "Customer asks for a refund", "priority": "high", "userId": 6 } } ``` ### `tag_added` A tag is added to a contact. Group: Contacts. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `tag` | The tag added. | | `userId` | Workspace user id. | Example delivery: ```json { "event": "tag_added", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "tag": "vip", "userId": 6 } } ``` ### `message_status` An outbound message is delivered, read or fails, with the WhatsApp timestamp. Requires API access (Scale plan). Group: Messages. | Field in `data` | Meaning | | --- | --- | | `message_id` | WhatsApp message id (wamid). | | `phone` | Customer phone number. | | `status` | sent, delivered, read or failed. | | `at` | When WhatsApp reported the status (ISO 8601, UTC). | | `error` | Failure reason when status is failed, else null. | Example delivery: ```json { "event": "message_status", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "message_id": "wamid.HBgM...B7", "phone": "919876543210", "status": "read", "at": "2026-10-02T09:22:48.000Z", "error": null } } ``` ### `conversation_seen` A team member opens a chat in the inbox or the embedded panel. Requires API access (Scale plan). Group: Conversations. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `user` | { user_id, name } of the team member who opened it. | | `opened_at` | ISO 8601, UTC. | | `marked_read` | How many unread customer messages this open marked as read. | Example delivery: ```json { "event": "conversation_seen", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "user": { "user_id": 41, "name": "Anita" }, "opened_at": "2026-10-02T09:15:30.000Z", "marked_read": 3 } } ``` ### `conversation_assigned` A chat is claimed, released or transferred. Requires API access (Scale plan). Group: Conversations. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `action` | claimed, released or transferred. | | `assigned_to` | { user_id, name } now holding the chat, or null after a release. | | `previous` | { user_id, name } who held it before, or null. | | `by` | { user_id, name } who did it, or null when the system did. | | `reason` | Why, for example human_claimed or handoff_transfer. | | `at` | ISO 8601, UTC. | Example delivery: ```json { "event": "conversation_assigned", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "action": "transferred", "assigned_to": { "user_id": 52, "name": "Vikram" }, "previous": { "user_id": 41, "name": "Anita" }, "by": { "user_id": 41, "name": "Anita" }, "reason": "handoff_transfer", "at": "2026-10-02T09:40:11.000Z" } } ``` ### `conversation_resolved` A chat is resolved and handed back to the AI. Requires API access (Scale plan). Group: Conversations. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `by` | { user_id, name } who resolved it, or null. | | `previous` | { user_id, name } who held it, or null. | | `at` | ISO 8601, UTC. | Example delivery: ```json { "event": "conversation_resolved", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "by": { "user_id": 41, "name": "Anita" }, "previous": { "user_id": 41, "name": "Anita" }, "at": "2026-10-02T10:02:00.000Z" } } ``` ### `sla_breached` A chat misses its response target. Requires API access (Scale plan). Group: Team. | Field in `data` | Meaning | | --- | --- | | `phone` | Customer phone number. | | `breach_type` | Which SLA timer was missed. | | `threshold_seconds` | The target that was missed. | | `timer_start` | When the timer started (ISO 8601, UTC). | | `policy_id` | The SLA policy that applied. | | `at` | ISO 8601, UTC. | Example delivery: ```json { "event": "sla_breached", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "phone": "919876543210", "breach_type": "first_response", "threshold_seconds": 900, "timer_start": "2026-10-02T09:14:07.000Z", "policy_id": 3, "at": "2026-10-02T09:29:07.000Z" } } ``` ### `credits_threshold` AI credit use crosses a level (80 and 100 percent by default) of the month’s allocation. Requires API access (Scale plan). Group: Billing. | Field in `data` | Meaning | | --- | --- | | `level` | The percentage crossed. | | `used_credits` | Credits used this cycle. | | `allocation` | Bundle plus active top-ups. | | `remaining_credits` | Credits left, never below zero. | | `cycle_start` | Start of the monthly cycle (ISO 8601, UTC). | Example delivery: ```json { "event": "credits_threshold", "sent_at": "2026-10-02T09:22:49.000Z", "correlation_id": "iwa_m1x2y3z4_a1b2c3d4e5f6", "actor": "inboxwa", "data": { "level": 80, "used_credits": 10000, "allocation": 12500, "remaining_credits": 2500, "cycle_start": "2026-09-30T18:30:00.000Z" } } ``` ## Changelog ### 2026-09-26: The API follows the plan - Starter has no API and no webhooks. Growth has the core API (sending messages, creating and updating contacts, listing templates, Zapier) and five webhook events. Scale has every call, the Reporting API and all webhook events. See Plans and API access. - A call outside the plan is refused with `api_not_in_plan` (Starter) or `api_access_not_enabled` (a Growth key calling the full API), whatever scopes the key has. - `GET /api/reporting/v1/whoami` and the key list now report `api_tier`: `none`, `core` or `full`. - New: List message templates, `GET /api/templates`. ### 2026-09-26: Documentation audit - The Overview no longer says every contact call works on every plan: sending and changing contacts do, reading contacts and chats with a key needs API access (Scale plan). - Phone numbers: a 10-digit number without a country code is treated as Indian (91 is added). Always send the country code. - Keys are created on the API Keys page in the sidebar (the docs and one error message said Settings). Key-management calls can also be made with a full-access key created by an admin. - The Errors guide now separates stable snake_case codes from the sentences older endpoints return, lists the codes the API really uses, and explains that a WhatsApp refusal on a send is a 500 with `errorCode`. - The Rate limits guide describes both limiters and their different response shapes. - Webhooks: the signature covers the body only, so the guide now says how to reject replays with `sent_at` and `correlation_id`, and states plainly that an event that fails all attempts is not redelivered. - The MCP guide now says it uses its own token, not an API key, and that actions needing approval cannot be exposed. ### 2026-09-25: One place for the API documentation - Messaging and Contacts are now documented here in full: send a message or a broadcast, and create, read, update and delete contacts, their tags, custom attributes and consent. - A guide to creating and managing keys, and an Integrations guide, replace the separate API Keys help page. The old address now opens this page. - `POST /api/send-message` now documents every message type and the response it really returns. - Sending (`/api/send-message`, `/api/bulk-send`) is not held to the default per-key request limit. A limit you set on a key still applies to everything. ### 2026-09-25: Reporting API v1, key controls and webhook events - Reporting API at `/api/reporting/v1`: conversations, unread and unanswered, seen-by, delivery and read receipts, replies, first-response metrics, assignment history. - Read-only scopes (`inbox:read`, `contacts:read`, `team_management:read`, `analytics:read` and more). `inbox_view` now works as a read scope for keys. - Key expiry, per-key rate limits with standard headers, rename, and rotation with a grace period. - Refused and rate-limited calls now appear in the per-key audit log. - Six new webhook events: `message_status`, `conversation_seen`, `conversation_assigned`, `conversation_resolved`, `sla_breached`, `credits_threshold`. - Webhook signing secret rotation, a test-send endpoint, and immediate application of saved changes. - Contact and chat-history reads for API keys are now controlled by the `api_access` plan feature rather than being restricted to one workspace.