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. Machine-readable: OpenAPI 3.0 document, this reference as Markdown, index for AI assistants. Each guide and call also has its own Markdown page, linked below.
Guides
Overview
Markdown: 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.
- 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.
Plans and API access
Markdown: 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 messagePOST /api/bulk-send: Send a broadcastGET /api/templates: List message templatesPOST /api/contacts: Create or update a contactPUT /api/contacts/:phone/name: Set a contact’s namePUT /api/contacts/:phone/status: Set a contact’s statusPUT /api/contacts/:phone/email: Set a contact’s emailPUT /api/contacts/:phone/company: Set a contact’s companyPUT /api/contacts/:phone/tags: Set a contact’s tagsPUT /api/contacts/:phone/notes: Set a contact’s notesPUT /api/contacts/:phone/attributes: Merge custom attributesDELETE /api/contacts/:phone/attributes/:key: Delete a custom attributePOST /api/contacts/:phone/opt-out: Mark a contact opted outPOST /api/contacts/:phone/opt-in: Re-subscribe a contact/api/zapier/*(any method): Zapier triggers and actionsGET /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
Markdown: 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_apiscope (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. - Check it.
GET /api/reporting/v1/whoamireturns 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-messagewith a template, or with text inside the 24-hour window. It needs thewhatsapp_apiscope.
Check the key, then send a message (curl; the Markdown version has Node.js, Python, PHP too)
# 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"]}}'
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; the Markdown version has Node.js, Python, PHP too)
curl "https://inboxwa.online/api/reporting/v1/conversations/unanswered?older_than=30m" \
-H "Authorization: Bearer iwk_yourkeyhere"Authentication
Markdown: 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. - 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.
API keys
Markdown: 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.
- 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.
- 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.
Scopes and permissions
Markdown: 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. |
Rate limits
Markdown: 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.
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
{
"error": "rate_limited",
"message": "This key is limited to 600 requests per minute. Retry after 12s.",
"retry_after_seconds": 12
}
- Back off for
Retry-Afterseconds, 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
Markdown: 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
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
doneErrors
Markdown: 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
{
"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
Markdown: 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.
What is recorded, and from when
- Receipts. Delivery and read times are recorded from the day recording began (the
recorded_fromfield 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
Markdown: 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
{
"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; the Markdown version has Python, PHP too)
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);
}
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
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 exampleGET /api/reporting/v1/messageswithsinceset 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_idand order by the timestamps indata. - Use
POST /api/webhook/outbound/testto send a signed test event, andGET /api/webhook/outbound/deliveriesto see what your endpoint answered. - Rotate the signing secret with
POST /api/webhook/outbound/rotate-secret. The new secret is shown once.
Integrations
Markdown: 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.
- Make builds multi-step scenarios with HTTP modules. Make guide.
- Inbound webhooks let another system push events into InboxWA. Webhooks guide and Webhook builder.
- Shopify, Google Sheets and Razorpay have their own setup pages: Shopify, Google Sheets, Razorpay.
- WHMCS connects hosting billing and support to the inbox. WHMCS connector.
AI tools (MCP)
Markdown: 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/listandtools/callare 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). Markdown: https://inboxwa.online/docs/api/op/sendMessage.md
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. |
{
"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.
{
"success": true,
"messageId": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSQTEB",
"data": {
"messaging_product": "whatsapp",
"contacts": [
{
"input": "919876543210",
"wa_id": "919876543210"
}
],
"messages": [
{
"id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgSQTEB"
}
]
}
}
Errors specific to this call
| 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. |
Send a broadcast
POST /api/bulk-send (operationId bulkSend). Markdown: https://inboxwa.online/docs/api/op/bulkSend.md
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. |
{
"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.
{
"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
| 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. |
List message templates
GET /api/templates (operationId listTemplates). Markdown: https://inboxwa.online/docs/api/op/listTemplates.md
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.
{
"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
| Status | error | When |
|---|---|---|
| 403 | not_connected | WhatsApp is not connected for this workspace. |
| 500 | Failed to fetch templates | WhatsApp did not answer. |
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). Markdown: https://inboxwa.online/docs/api/op/listContacts.md
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.
{
"success": true,
"contacts": [
{
"phone": "919876543210",
"name": "Ravi Kumar",
"email": "[email protected]",
"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
}
}
Search contacts
GET /api/contacts/search (operationId searchContacts). Markdown: https://inboxwa.online/docs/api/op/searchContacts.md
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.
{
"success": true,
"contacts": [
{
"phone": "919876543210",
"name": "Ravi Kumar",
"tags": "vip,hosting"
}
],
"total": 1
}
List contacts (plain array)
GET /api/contacts (operationId listAllContacts). Markdown: https://inboxwa.online/docs/api/op/listAllContacts.md
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.
[
{
"phone": "919876543210",
"name": "Ravi Kumar",
"status": "New",
"tags": "vip,hosting"
}
]
Create or update a contact
POST /api/contacts (operationId createContact). Markdown: https://inboxwa.online/docs/api/op/createContact.md
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. |
{
"phone": "919876543210",
"name": "Ravi Kumar",
"email": "[email protected]",
"company": "Kumar Hosting",
"tags": "vip,hosting"
}
Response 200
Saved.
{
"success": true,
"message": "Contact successfully saved."
}
Errors specific to this call
| 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. |
Set a contact’s name
PUT /api/contacts/{phone}/name (operationId updateContactName). Markdown: https://inboxwa.online/docs/api/op/updateContactName.md
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... |
{
"name": "Ravi Kumar"
}
Response 200
Saved.
{
"success": true,
"name": "Ravi Kumar"
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 400 | Name is required | name is empty. |
| 412 | version_conflict | Someone else changed the contact since the version you sent. |
Set a contact’s status
PUT /api/contacts/{phone}/status (operationId updateContactStatus). Markdown: https://inboxwa.online/docs/api/op/updateContactStatus.md
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... |
{
"status": "Qualified"
}
Response 200
Saved.
{
"success": true,
"status": "Qualified"
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 400 | Status is required | status is empty. |
| 412 | version_conflict | Someone else changed the contact since the version you sent. |
Set a contact’s email
PUT /api/contacts/{phone}/email (operationId updateContactEmail). Markdown: https://inboxwa.online/docs/api/op/updateContactEmail.md
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... |
{
"email": "[email protected]"
}
Response 200
Saved.
{
"success": true,
"email": "[email protected]"
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 412 | version_conflict | Someone else changed the contact since the version you sent. |
Set a contact’s company
PUT /api/contacts/{phone}/company (operationId updateContactCompany). Markdown: https://inboxwa.online/docs/api/op/updateContactCompany.md
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... |
{
"company": "Kumar Hosting"
}
Response 200
Saved.
{
"success": true,
"company": "Kumar Hosting"
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 412 | version_conflict | Someone else changed the contact since the version you sent. |
Set a contact’s tags
PUT /api/contacts/{phone}/tags (operationId updateContactTags). Markdown: https://inboxwa.online/docs/api/op/updateContactTags.md
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... |
{
"tags": "vip,hosting"
}
Response 200
Saved.
{
"success": true,
"tags": "vip,hosting"
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 412 | version_conflict | Someone else changed the contact since the version you sent. |
Set a contact’s notes
PUT /api/contacts/{phone}/notes (operationId updateContactNotes). Markdown: https://inboxwa.online/docs/api/op/updateContactNotes.md
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... |
{
"notes": "Asked for a callback on Monday."
}
Response 200
Saved.
{
"success": true,
"notes": "Asked for a callback on Monday."
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 412 | version_conflict | Someone else changed the contact since the version you sent. |
Get custom attributes
GET /api/contacts/{phone}/attributes (operationId getContactAttributes). Markdown: https://inboxwa.online/docs/api/op/getContactAttributes.md
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.
{
"success": true,
"attributes": {
"plan": "Business",
"renewal": "2026-12-01"
}
}
Merge custom attributes
PUT /api/contacts/{phone}/attributes (operationId setContactAttributes). Markdown: https://inboxwa.online/docs/api/op/setContactAttributes.md
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. |
{
"attributes": {
"plan": "Business",
"renewal": "2026-12-01"
}
}
Response 200
The merged attributes.
{
"success": true,
"attributes": {
"plan": "Business",
"renewal": "2026-12-01"
}
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 400 | attributes object is required | attributes is missing or not an object. |
Delete a custom attribute
DELETE /api/contacts/{phone}/attributes/{key} (operationId deleteContactAttribute). Markdown: https://inboxwa.online/docs/api/op/deleteContactAttribute.md
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.
{
"success": true,
"attributes": {
"plan": "Business"
}
}
Mark a contact opted out
POST /api/contacts/{phone}/opt-out (operationId optOutContact). Markdown: https://inboxwa.online/docs/api/op/optOutContact.md
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.
{
"success": true
}
Re-subscribe a contact
POST /api/contacts/{phone}/opt-in (operationId optInContact). Markdown: https://inboxwa.online/docs/api/op/optInContact.md
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.
{
"success": true
}
List opted-out contacts
GET /api/contacts/opted-out (operationId listOptedOut). Markdown: https://inboxwa.online/docs/api/op/listOptedOut.md
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.
{
"success": true,
"data": [
{
"phone": "919812345678",
"name": "Asha Rao",
"tags": "newsletter",
"updated_at": "2026-09-30T11:02:00.000Z"
}
],
"count": 1
}
Mark a chat read
POST /api/contacts/{phone}/read (operationId markRead). Markdown: https://inboxwa.online/docs/api/op/markRead.md
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.
{
"success": true
}
Delete a contact
DELETE /api/contacts/{phone} (operationId deleteContact). Markdown: https://inboxwa.online/docs/api/op/deleteContact.md
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.
{
"success": true,
"deleted": true
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 404 | Contact not found | No such contact, or not one the caller can see. |
Delete many contacts
POST /api/contacts/bulk-delete (operationId bulkDeleteContacts). Markdown: https://inboxwa.online/docs/api/op/bulkDeleteContacts.md
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. |
{
"phones": [
"919876543210",
"919812345678"
]
}
Response 200
How many were deleted out of how many were asked for.
{
"success": true,
"deleted": 2,
"total": 2
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 400 | phones array required | phones is missing or empty. |
| 400 | max 500 phones per call | More than 500 numbers. |
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). Markdown: https://inboxwa.online/docs/api/op/whoami.md
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
| 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. |
{
"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"
}
List conversations
GET /api/reporting/v1/conversations (operationId listConversations). Markdown: https://inboxwa.online/docs/api/op/listConversations.md
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
| 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. |
{
"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
| 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. |
Unread and waiting totals
GET /api/reporting/v1/conversations/unread-summary (operationId unreadSummary). Markdown: https://inboxwa.online/docs/api/op/unreadSummary.md
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
| 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 |
{
"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"
}
}
Unanswered conversations
GET /api/reporting/v1/conversations/unanswered (operationId unansweredConversations). Markdown: https://inboxwa.online/docs/api/op/unansweredConversations.md
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
{
"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
| Status | error | When |
|---|---|---|
| 400 | invalid_older_than | older_than is not a duration. |
Get a conversation
GET /api/reporting/v1/conversations/{phone} (operationId getConversation). Markdown: https://inboxwa.online/docs/api/op/getConversation.md
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
{
"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": "[email protected]",
"company": "Kumar Hosting",
"status": "New",
"tags": [
"vip",
"hosting"
]
}
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 404 | not_found | No such chat in this workspace, or it is not assigned to a member limited to assigned chats. |
List messages in a conversation
GET /api/reporting/v1/conversations/{phone}/messages (operationId listMessages). Markdown: https://inboxwa.online/docs/api/op/listMessages.md
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
| 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 |
{
"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
| Status | error | When |
|---|---|---|
| 400 | invalid_order | order is not asc or desc. |
Receipts for a conversation
GET /api/reporting/v1/conversations/{phone}/receipts (operationId conversationReceipts). Markdown: https://inboxwa.online/docs/api/op/conversationReceipts.md
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
| 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. |
{
"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"
}
]
}
]
}
Who has opened a chat
GET /api/reporting/v1/conversations/{phone}/seen-by (operationId seenBy). Markdown: https://inboxwa.online/docs/api/op/seenBy.md
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
| 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. |
{
"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"
}
Open log for a chat
GET /api/reporting/v1/conversations/{phone}/views (operationId listViews). Markdown: https://inboxwa.online/docs/api/op/listViews.md
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
| 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 |
{
"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
}
Replies in a conversation
GET /api/reporting/v1/conversations/{phone}/replies (operationId conversationReplies). Markdown: https://inboxwa.online/docs/api/op/conversationReplies.md
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
| 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 |
{
"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
}
}
Who has held a chat
GET /api/reporting/v1/conversations/{phone}/assignment-history (operationId assignmentHistory). Markdown: https://inboxwa.online/docs/api/op/assignmentHistory.md
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
| 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. |
{
"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"
}
Messages by delivery status
GET /api/reporting/v1/messages (operationId listMessagesByStatus). Markdown: https://inboxwa.online/docs/api/op/listMessagesByStatus.md
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
| 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. |
{
"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
| Status | error | When |
|---|---|---|
| 400 | invalid_status | status is missing or not recognised. |
| 400 | window_too_large | The window is longer than 90 days. |
Get a message
GET /api/reporting/v1/messages/{id} (operationId getMessage). Markdown: https://inboxwa.online/docs/api/op/getMessage.md
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
{
"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
| Status | error | When |
|---|---|---|
| 404 | not_found | No such message in this workspace. |
Delivery and read receipts
GET /api/reporting/v1/messages/{id}/receipts (operationId messageReceipts). Markdown: https://inboxwa.online/docs/api/op/messageReceipts.md
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
| 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. |
{
"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
| Status | error | When |
|---|---|---|
| 404 | not_found | No such message in this workspace. |
Replies across the workspace
GET /api/reporting/v1/replies (operationId listReplies). Markdown: https://inboxwa.online/docs/api/op/listReplies.md
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
| 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. |
{
"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
| 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. |
First response times
GET /api/reporting/v1/metrics/first-response (operationId firstResponseMetrics). Markdown: https://inboxwa.online/docs/api/op/firstResponseMetrics.md
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
| 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. |
{
"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
| 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. |
Chats a team member has not opened
GET /api/reporting/v1/team/{userId}/unseen (operationId unseenForMember). Markdown: https://inboxwa.online/docs/api/op/unseenForMember.md
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
| 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 |
{
"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
| Status | error | When |
|---|---|---|
| 400 | invalid_user_id | userId is not a number. |
| 404 | not_found | No such team member in this workspace. |
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). Markdown: https://inboxwa.online/docs/api/op/teamRoster.md
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.
{
"members": [
{
"user_id": 41,
"display_name": "Anita",
"role": "agent"
}
]
}
Team performance
GET /api/team/v2/performance (operationId teamPerformance). Markdown: https://inboxwa.online/docs/api/op/teamPerformance.md
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.
{
"range": "7d",
"members": []
}
SLA dashboard
GET /api/team/v2/sla/dashboard (operationId slaDashboard). Markdown: https://inboxwa.online/docs/api/op/slaDashboard.md
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.
{
"range": "7d"
}
Hand-off queue
GET /api/team/v2/handoff (operationId handoffQueue). Markdown: https://inboxwa.online/docs/api/op/handoffQueue.md
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.
{
"queue": []
}
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). Markdown: https://inboxwa.online/docs/api/op/getWebhookConfig.md
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
| 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 |
{
"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": "[email protected]",
"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
]
}
Save webhook settings
PUT /api/webhook/outbound/config (operationId putWebhookConfig). Markdown: https://inboxwa.online/docs/api/op/putWebhookConfig.md
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]. |
{
"endpoint_url": "https://example.com/hooks/inboxwa",
"events": [
"message_received",
"message_status",
"credits_threshold"
],
"credit_alert_levels": [
80,
100
]
}
Response 200
Saved.
{
"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
| 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. |
Rotate the signing secret
POST /api/webhook/outbound/rotate-secret (operationId rotateWebhookSecret). Markdown: https://inboxwa.online/docs/api/op/rotateWebhookSecret.md
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.
{
"success": true,
"signing_secret": "41ab...07cd",
"message": "Signing secret rotated. Save it now; it will not be shown again."
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 404 | not_configured | No webhook is configured. |
Send a test event
POST /api/webhook/outbound/test (operationId testWebhook). Markdown: https://inboxwa.online/docs/api/op/testWebhook.md
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. |
{
"event": "message_status"
}
Response 200
OK
| Field | Type | Description |
|---|---|---|
ok | boolean | |
status | integer, nullable | HTTP status your endpoint returned. |
attempts | integer | |
error | string, nullable | |
duration_ms | integer | |
event | string |
{
"ok": true,
"status": 200,
"attempts": 1,
"error": null,
"duration_ms": 184,
"event": "message_status"
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 400 | unknown_event | Not a catalogue event. |
| 404 | not_configured | Save an endpoint first. |
Delivery log
GET /api/webhook/outbound/deliveries (operationId webhookDeliveries). Markdown: https://inboxwa.online/docs/api/op/webhookDeliveries.md
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.
{
"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"
}
]
}
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). Markdown: https://inboxwa.online/docs/api/op/listApiKeys.md
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
| 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. |
{
"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": "[email protected]",
"revoked_by_name": null
}
],
"defaults": {
"rate_limit_per_min": 600,
"max_keys": 20
},
"api_access": true,
"api_tier": "full"
}
Create a key
POST /api/api-keys (operationId createApiKey). Markdown: https://inboxwa.online/docs/api/op/createApiKey.md
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. |
{
"label": "Dashboard",
"scopes": [
"inbox:read",
"team_management:read"
],
"expires_in_days": 90,
"rate_limit_per_min": 120
}
Response 200
OK
| 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 |
{
"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
| 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. |
List scopes
GET /api/api-keys/scopes (operationId apiKeyScopes). Markdown: https://inboxwa.online/docs/api/op/apiKeyScopes.md
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
| 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 |
{
"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"
]
}
Update a key
PATCH /api/api-keys/{id} (operationId updateApiKey). Markdown: https://inboxwa.online/docs/api/op/updateApiKey.md
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 |
{
"label": "Dashboard (prod)",
"rate_limit_per_min": 300
}
Response 200
Updated.
{
"success": true
}
Errors specific to this call
| 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. |
Revoke a key
DELETE /api/api-keys/{id} (operationId revokeApiKey). Markdown: https://inboxwa.online/docs/api/op/revokeApiKey.md
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.
{
"success": true
}
Errors specific to this call
| Status | error | When |
|---|---|---|
| 404 | not_found | No such key. |
| 400 | already_revoked | Already revoked. |
Rotate a key
POST /api/api-keys/{id}/rotate (operationId rotateApiKey). Markdown: https://inboxwa.online/docs/api/op/rotateApiKey.md
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. |
{
"grace_hours": 24
}
Response 200
OK
| 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 |
{
"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
| 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. |
Key activity
GET /api/api-keys/{id}/usage (operationId apiKeyUsage). Markdown: https://inboxwa.online/docs/api/op/apiKeyUsage.md
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
| 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 |
{
"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"
}
]
}
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). Markdown: https://inboxwa.online/docs/api/op/getCredits.md
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
| 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 |
{
"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
}
Credit ledger
GET /api/agent-hub/credits/ledger (operationId getCreditsLedger). Markdown: https://inboxwa.online/docs/api/op/getCreditsLedger.md
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.
{
"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
}
Webhook events
message_received
A customer message arrives.
| 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. |
{
"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).
| 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. |
{
"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.
| Field in data | Meaning |
|---|---|
phone | Customer phone number. |
userId | Workspace user id. |
{
"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.
| Field in data | Meaning |
|---|---|
phone | Customer phone number. |
note | Handoff note, truncated to 500 characters. |
priority | normal or high. |
userId | Workspace user id. |
{
"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.
| Field in data | Meaning |
|---|---|
phone | Customer phone number. |
tag | The tag added. |
userId | Workspace user id. |
{
"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).
| 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. |
{
"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).
| 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. |
{
"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).
| 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. |
{
"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).
| 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. |
{
"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).
| 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. |
{
"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).
| 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). |
{
"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) orapi_access_not_enabled(a Growth key calling the full API), whatever scopes the key has. GET /api/reporting/v1/whoamiand the key list now reportapi_tier:none,coreorfull.- 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_atandcorrelation_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-messagenow 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:readand more).inbox_viewnow 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_accessplan feature rather than being restricted to one workspace.