Agent Inbox API — Read and Send WhatsApp Inbox Messages
The Agent Inbox API gives an API key access to the WhatsApp Inbox. An AI agent or automation tool can read conversations and send messages through this API. It can also hand a conversation back to your team, next to your human agents in the same Inbox.
This is a separate API from Agent API v2. Add an Inbox scope to any workspace API key, including one that already holds Agent API v2 scopes.
Scopes
Create an API key in Settings → API Keys and add one or both Inbox scopes. Scopes are fixed when the key is created.
| Scope | Access |
|---|---|
inbox.read | Read conversations, messages, and approved templates. |
inbox.write | Send messages, mark a conversation read, change its owner or status, add labels, and add a note. |
Key options
Set these options on the key, not on each request.
- Connection list: restrict the key to one or more WhatsApp connections. Leave it empty to allow every connection in the workspace. Every route only returns or accepts conversations on the allowed connections.
- Phone visibility: mask the customer phone number in every response. A masked number keeps the first 5 and the last 3 digits. For example,
628123456789becomes62812****789.
Authenticate
Send the key as a bearer token on every request.
Authorization: Bearer <your_api_key>
| Status | Meaning |
|---|---|
401 | The key is missing, invalid, expired, revoked, or does not have the needed scope. |
403 | Your plan does not allow this access. |
503 | The Inbox is not available for this workspace right now. |
The conversation owner
Every conversation has one owner: human, or agent:<api_key_id> for an agent key.
- A new conversation gets an owner automatically. Konektor picks the oldest active key with the
inbox.writescope that allows the connection. No matching key leaves the owner ashuman. - Your key can send a message only on a conversation it owns. A send on a
human-owned or another agent's conversation returns409 OWNER_MISMATCH. - A message from a team member always takes a conversation back to
human. - Your key can also request the handoff directly. See Update a conversation. It may set
ownertohumanor to its ownagent:<api_key_id>, never to another agent's id. - Every owner change fires the
inbox.conversation.owner_changedwebhook event.
The unofficial channel needs a current acknowledgement from a workspace member before your key can send on it. A send without one returns 409 ACKNOWLEDGEMENT_REQUIRED.
The conversation object
{
"id": "7c9ecb52-1f2a-4b3f-9b3e-2b6a2c8f9a11",
"connection_id": "conn_abc123",
"channel": "waba",
"owner": "agent:ak_9f21",
"status": "open",
"assigned_to_user_id": null,
"label_ids": ["vip"],
"unread_count": 1,
"window_expires_at": "2026-09-17T14:00:00.000Z",
"snoozed_until": null,
"last_message_at": "2026-09-17T09:58:00.000Z",
"updated_at": "2026-09-17T09:58:01.000Z",
"contact": { "name": "Budi", "phone": "628123456789" }
}
| Field | Meaning |
|---|---|
channel | waba (official Cloud API), unofficial (QR session), or partner (a connected partner number). |
owner | human or agent:<api_key_id>. See The conversation owner. |
status | open, pending, snoozed, resolved, or closed. |
window_expires_at | When the 24-hour conversation window closes. null when it is already closed. |
contact.phone | Masked when your key has phone visibility set to masked. |
The message object
{
"id": "msg_9f2a1b",
"external_id": "wamid.HBgMNjI4MTIzNDU2Nzg5FQIAERgS",
"direction": "outbound",
"kind": "text",
"text": "Thank you, your order is on its way.",
"media": null,
"status": "sent",
"error_code": null,
"actor": { "type": "agent", "id": "ak_9f21" },
"occurred_at": "2026-09-17T09:58:00.000Z",
"referral": null
}
| Field | Meaning |
|---|---|
direction | inbound (from the customer) or outbound (to the customer). |
kind | text, template, image, video, audio, document, location, or system. You can only send text or template. |
media | Always null. kind tells you the message type. Konektor does not return media bytes through this API. |
status | pending, sent, delivered, read, or failed. |
actor | { "type": "agent", "id": "<api_key_id>" }, { "type": "user", "id": "<user_id>" }, or null for an inbound message. |
referral | Click-to-WhatsApp ad details for the message that opened the conversation, or null. |
referral.ctwa_clid | The ad click id, or null. |
referral.source_id | The ad or post id, or null. |
referral.source_type | The ad source type, or null. |
referral.source_url | The ad source URL, or null. |
referral.headline | The ad headline text, or null. |
Routes overview
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /api/v1/inbox/conversations | inbox.read | List conversations |
GET | /api/v1/inbox/conversations/{id} | inbox.read | Get one conversation |
GET | /api/v1/inbox/conversations/{id}/messages | inbox.read | List messages, oldest first |
POST | /api/v1/inbox/conversations/{id}/messages | inbox.write | Send a text or template message |
POST | /api/v1/inbox/conversations/{id}/read | inbox.write | Mark a conversation read |
PATCH | /api/v1/inbox/conversations/{id} | inbox.write | Change owner, status, or labels, or add a note |
GET | /api/v1/inbox/templates | inbox.read | List approved templates |
A conversation your key's connection list does not allow returns 404 NOT_FOUND on every route.
List conversations
GET /api/v1/inbox/conversations
Scope: inbox.read
| Query parameter | Notes |
|---|---|
connection_id | Limit results to one connection. |
owner | human, agent (any agent), or agent:<api_key_id>. |
status | One conversation status. |
updated_after | ISO date-time. Only conversations updated after this moment. |
limit | 1-100. Default 30. |
cursor | From a previous response. Omit it for the first page. |
{
"conversations": [ { "id": "7c9ecb52-...", "...": "..." } ],
"cursor": "eyJ0IjoxNzU4MDk1..."
}
Call the route again with cursor set to the returned value to read the next page. A null cursor means there is no next page.
Get one conversation
GET /api/v1/inbox/conversations/{id}
Scope: inbox.read. Returns { "conversation": { ... } }.
List messages
GET /api/v1/inbox/conversations/{id}/messages
Scope: inbox.read
| Query parameter | Notes |
|---|---|
limit | 1-100. Default 50. |
before | ISO date-time. Only messages that occurred before this moment. |
{
"messages": [ { "id": "msg_1", "...": "..." } ],
"has_more": false
}
Messages come oldest first. To read older messages, call again with before set to the occurred_at value of the oldest message you already have.
Send a message
POST /api/v1/inbox/conversations/{id}/messages
Scope: inbox.write. Send an Idempotency-Key header with every request.
Idempotency-Key: <a value unique to this send>
Retry with the same Idempotency-Key after a timeout or a network error. Konektor sends the message only once. A missing header returns 400 IDEMPOTENCY_KEY_REQUIRED.
Send a text message when the 24-hour conversation window is open. text takes up to 16,384 characters:
{
"kind": "text",
"text": "Thank you, your order is on its way.",
"preview_url": true
}
Send an approved template to open or restart a conversation. Send up to 50 body_params values, each 2,048 characters or fewer:
{
"kind": "template",
"template": {
"name": "order_confirmation",
"language": "en",
"body_params": ["Budi", "INV-2049"]
}
}
List body_params in order. Each value fills one placeholder, in order: {{1}}, {{2}}, and so on. See List templates for the placeholder count of each template.
Both requests return 202 Accepted:
{ "message_id": "msg_9f2a1b", "status": "sent" }
status is sent or failed. Delivery updates arrive through the inbox.message.status webhook. A repeat of the same Idempotency-Key returns the first result, also when that result is failed. Use a new key to send again.
kind: "interactive" is not supported and returns 422 UNSUPPORTED_KIND. Send text or template instead.
Mark a conversation read
POST /api/v1/inbox/conversations/{id}/read
Scope: inbox.write. Returns { "id": "7c9ecb52-...", "unread_count": 0 }.
Update a conversation
PATCH /api/v1/inbox/conversations/{id}
Scope: inbox.write. Send at least one field.
| Field | Notes |
|---|---|
owner | human or your own agent:<api_key_id>. Another agent's id returns 403 OWNER_FORBIDDEN. |
status | A new conversation status. |
label_ids | Up to 20 label ids. |
note | 1-4096 characters. Adds an internal note. It does not send a message to the customer. |
Take a conversation over:
{ "owner": "agent:ak_9f21" }
Hand it back to your team:
{ "owner": "human" }
Returns { "conversation": { ... } } with the fresh state.
List templates
GET /api/v1/inbox/templates
Scope: inbox.read. Optional query parameter: connection_id.
{
"templates": [
{
"id": "tpl_1",
"connection_id": "conn_abc123",
"name": "order_confirmation",
"language": "en",
"category": "UTILITY",
"body": "Hi {{1}}, we received your order #{{2}}.",
"parameter_count": 2
}
]
}
Only approved templates appear. parameter_count is the number of body_params values a send needs.
What this API does not support
- Interactive messages (buttons and lists). Send
textortemplateinstead. - Template header parameters. Only body parameters are available.
- Media bytes, in a message or in a webhook payload.
kindtells you the type. - A typing indicator.
POST .../readmarks the conversation read only.
Errors
Every error returns a JSON body with a code field.
{
"statusCode": 409,
"statusMessage": "OWNER_MISMATCH",
"data": { "code": "OWNER_MISMATCH" }
}
Read data.code to branch your error handling.
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_QUERY | A query parameter does not match the expected type or range. |
| 400 | INVALID_REQUEST | The request body does not match the expected shape. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing. |
| 401 | — | The key is missing, invalid, expired, revoked, or lacks the needed scope. |
| 403 | — | Your plan does not allow this access. |
| 403 | OWNER_FORBIDDEN | You tried to set owner to another agent's id. |
| 404 | NOT_FOUND | The conversation does not exist, or your key's connection list does not allow it. |
| 409 | OWNER_MISMATCH | Your key does not own this conversation. |
| 409 | CONNECTION_UNAVAILABLE | The WhatsApp connection cannot send right now. |
| 409 | ACKNOWLEDGEMENT_REQUIRED | The unofficial channel needs a current acknowledgement from a workspace member. |
| 409 | NOT_RETRYABLE | This message cannot be retried. |
| 422 | UNSUPPORTED_KIND | The message kind is interactive. |
| 422 | WINDOW_CLOSED | The 24-hour conversation window is closed. Send a template instead. |
| 422 | TEMPLATE_NOT_APPROVED | No approved template matches the given name and language. |
| 422 | INVALID_MESSAGE | The message did not pass validation. |
| 502 | DELIVERY_FAILED | WhatsApp did not deliver the message. |
| 503 | WHATSAPP_INBOX_UNAVAILABLE | The Inbox is not available for this workspace right now. |
Idempotency and rate limit
Send a unique Idempotency-Key header with every message send. Reuse the same value on a retry. Konektor sends the message only once for the same key.
The limit is 300 requests per minute. Above that the API answers 429. Rely on Idempotency-Key so a retry never sends a message twice.
Webhook events
The Inbox publishes four events, in addition to the lead.* events on Webhooks. They share the same delivery, headers, and signature verification. The body wraps the payload:
{ "event": "<event_name>", "payload": { "...": "..." } }
This differs from lead.* events, whose body is the payload alone.
inbox.message.received
Fires when a customer message arrives.
{
"event": "inbox.message.received",
"payload": {
"conversation": {
"id": "7c9ecb52-...",
"connection_id": "conn_abc123",
"channel": "waba",
"owner": "agent:ak_9f21",
"status": "open",
"window_expires_at": "2026-09-17T14:00:00.000Z"
},
"contact": { "phone": "628123456789", "name": "Budi" },
"message": {
"id": "msg_3",
"external_id": "wamid.HBg...",
"kind": "text",
"text": "Is my order shipped yet?",
"media": null,
"occurred_at": "2026-09-17T10:02:00.000Z"
},
"referral": null
}
}
inbox.message.sent
Fires for every outbound message, including one your own key sent. Compare actor.id to your own key id to skip your own messages.
{
"event": "inbox.message.sent",
"payload": {
"conversation": { "id": "7c9ecb52-...", "connection_id": "conn_abc123", "channel": "waba", "owner": "agent:ak_9f21" },
"message": { "id": "msg_4", "kind": "text", "text": "Yes, the tracking number is in your email.", "status": "sent", "occurred_at": "2026-09-17T10:03:00.000Z" },
"actor": { "type": "agent", "id": "ak_9f21" }
}
}
inbox.message.status
Fires when a message status changes, for example to delivered, read, or failed.
{
"event": "inbox.message.status",
"payload": {
"conversation_id": "7c9ecb52-...",
"message_id": "msg_4",
"status": "delivered",
"error_code": null,
"at": "2026-09-17T10:03:05.000Z"
}
}
inbox.conversation.owner_changed
Fires when a conversation's owner changes, from either a member action or a PATCH from your key.
{
"event": "inbox.conversation.owner_changed",
"payload": {
"conversation_id": "7c9ecb52-...",
"owner": "human",
"actor": { "type": "user", "id": "usr_182" },
"at": "2026-09-17T10:05:00.000Z"
}
}
Security
- Never send an API key in a prompt, URL, or log. Store it in your agent's secret manager.
- Encrypt any conversation or message data you store on your side.
- Ask for
inbox.readalone when your agent only needs to read the Inbox.
Next steps
Need More Help?
Our team is ready to help you maximize ad tracking and business attribution.
Try or continue
© 2026 Konektor. All rights reserved.
