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.

ScopeAccess
inbox.readRead conversations, messages, and approved templates.
inbox.writeSend 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, 628123456789 becomes 62812****789.

Authenticate

Send the key as a bearer token on every request.

Authorization: Bearer <your_api_key>
StatusMeaning
401The key is missing, invalid, expired, revoked, or does not have the needed scope.
403Your plan does not allow this access.
503The 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.write scope that allows the connection. No matching key leaves the owner as human.
  • Your key can send a message only on a conversation it owns. A send on a human-owned or another agent's conversation returns 409 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 owner to human or to its own agent:<api_key_id>, never to another agent's id.
  • Every owner change fires the inbox.conversation.owner_changed webhook 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" }
}
FieldMeaning
channelwaba (official Cloud API), unofficial (QR session), or partner (a connected partner number).
ownerhuman or agent:<api_key_id>. See The conversation owner.
statusopen, pending, snoozed, resolved, or closed.
window_expires_atWhen the 24-hour conversation window closes. null when it is already closed.
contact.phoneMasked 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
}
FieldMeaning
directioninbound (from the customer) or outbound (to the customer).
kindtext, template, image, video, audio, document, location, or system. You can only send text or template.
mediaAlways null. kind tells you the message type. Konektor does not return media bytes through this API.
statuspending, sent, delivered, read, or failed.
actor{ "type": "agent", "id": "<api_key_id>" }, { "type": "user", "id": "<user_id>" }, or null for an inbound message.
referralClick-to-WhatsApp ad details for the message that opened the conversation, or null.
referral.ctwa_clidThe ad click id, or null.
referral.source_idThe ad or post id, or null.
referral.source_typeThe ad source type, or null.
referral.source_urlThe ad source URL, or null.
referral.headlineThe ad headline text, or null.

Routes overview

MethodPathScopeDescription
GET/api/v1/inbox/conversationsinbox.readList conversations
GET/api/v1/inbox/conversations/{id}inbox.readGet one conversation
GET/api/v1/inbox/conversations/{id}/messagesinbox.readList messages, oldest first
POST/api/v1/inbox/conversations/{id}/messagesinbox.writeSend a text or template message
POST/api/v1/inbox/conversations/{id}/readinbox.writeMark a conversation read
PATCH/api/v1/inbox/conversations/{id}inbox.writeChange owner, status, or labels, or add a note
GET/api/v1/inbox/templatesinbox.readList 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 parameterNotes
connection_idLimit results to one connection.
ownerhuman, agent (any agent), or agent:<api_key_id>.
statusOne conversation status.
updated_afterISO date-time. Only conversations updated after this moment.
limit1-100. Default 30.
cursorFrom 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 parameterNotes
limit1-100. Default 50.
beforeISO 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.

FieldNotes
ownerhuman or your own agent:<api_key_id>. Another agent's id returns 403 OWNER_FORBIDDEN.
statusA new conversation status.
label_idsUp to 20 label ids.
note1-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 text or template instead.
  • Template header parameters. Only body parameters are available.
  • Media bytes, in a message or in a webhook payload. kind tells you the type.
  • A typing indicator. POST .../read marks 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.

StatusCodeMeaning
400INVALID_QUERYA query parameter does not match the expected type or range.
400INVALID_REQUESTThe request body does not match the expected shape.
400IDEMPOTENCY_KEY_REQUIREDThe Idempotency-Key header is missing.
401The key is missing, invalid, expired, revoked, or lacks the needed scope.
403Your plan does not allow this access.
403OWNER_FORBIDDENYou tried to set owner to another agent's id.
404NOT_FOUNDThe conversation does not exist, or your key's connection list does not allow it.
409OWNER_MISMATCHYour key does not own this conversation.
409CONNECTION_UNAVAILABLEThe WhatsApp connection cannot send right now.
409ACKNOWLEDGEMENT_REQUIREDThe unofficial channel needs a current acknowledgement from a workspace member.
409NOT_RETRYABLEThis message cannot be retried.
422UNSUPPORTED_KINDThe message kind is interactive.
422WINDOW_CLOSEDThe 24-hour conversation window is closed. Send a template instead.
422TEMPLATE_NOT_APPROVEDNo approved template matches the given name and language.
422INVALID_MESSAGEThe message did not pass validation.
502DELIVERY_FAILEDWhatsApp did not deliver the message.
503WHATSAPP_INBOX_UNAVAILABLEThe 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.read alone 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.

© 2026 Konektor. All rights reserved.