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 Pengaturan (Settings) > Lainnya (More) > API & Webhooks 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.
- Nomor WhatsApp yang boleh diakses (WhatsApp numbers this key can use): 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.
- Sembunyikan nomor pelanggan (Hide customer phone numbers): 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. - Wulan auto-reply does not reply to conversations that your key owns. Give the
inbox.writescope only to a key that does reply to chats. A key that only reads needs justinbox.read. - Do you want Wulan to reply first, and your agent to handle only some chats? Use the Agent API with the
agent.inbox.writescope. That scope does not take new conversations. The agent claims one conversation withowner: "self", and hands it back withowner: "wulan". - 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
ownertohuman, to its ownagent:<api_key_id>, or toagent:agentic(Wulan). It can never use another agent's id. - A conversation that Wulan owns shows
owner: "agent:agentic". Wulan replies to the next customer message. While Wulan auto-reply is off, a handoff to Wulan returns409 AUTO_REPLY_OFF. - 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.
Two integration patterns for other Meta apps
If you also run your own WhatsApp Cloud API app on the same number as your Konektor connection, pick one of these patterns.
Pattern 1 (recommended): send through Konektor
Send the reply through Send a message. Konektor calls WhatsApp itself, so the message is recorded right away, delivered/read status arrives automatically through the inbox.message.status webhook, and the conversation owner becomes your key (agent:<api_key_id>).
Short flow:
- Receive the
inbox.message.receivedwebhook. Storeconversation.idandconversation.owner. - If
owneris alreadyhuman, do not send an automatic reply. Take the conversation over first through Update a conversation if your policy allows it. - Send the reply through
POST /api/v1/inbox/conversations/{id}/messageswith anIdempotency-Keyheader. - If a team member takes the conversation over, the
inbox.conversation.owner_changedwebhook arrives withowner: "human". Stop replying automatically on that conversation.
A message sent this way shows in the Inbox with the caption "By AI".
Pattern 2: send through your own Meta app
During a transition, if you still send through your own WhatsApp Cloud API app, record a copy through Record a message another app sent. The Inbox then still shows the full conversation.
Limits of this pattern: the conversation stays owner: human, delivered/read status does not arrive, and a new conversation created from this record has no contact name until a later message arrives through Konektor. If you only have the customer's number, for example when you first set up the integration, look the conversation up first with the phone parameter on List conversations.
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 |
POST | /api/v1/inbox/echoes | inbox.write | Record a message another app sent on your number |
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/conversationsScope: inbox.read
| Query parameter | Notes |
|---|---|
connection_id | Limit results to one connection. |
phone | The customer's number, digits only, 8-20 digits. Returns the conversation whose number matches exactly, or an empty list when none does. An invalid number returns 400 INVALID_QUERY. |
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}/messagesScope: 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}/messagesScope: 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. Read the stored message's error_code before sending again.
If it is delivery_uncertain, check the provider conversation before creating another message or key. Do not retry automatically.
Use a new key only after a known failure, or after you confirm that the uncertain message was not delivered.
kind: "interactive" is not supported and returns 422 UNSUPPORTED_KIND. Send text or template instead.
Record a message another app sent
POST /api/v1/inbox/echoesScope: inbox.write. Use this route when your own WhatsApp Cloud API app sends a message on the same number. The inbox then shows that message in the conversation, and the conversation no longer counts as unanswered.
{
"connection_id": "conn_abc123",
"to": "628123456789",
"external_id": "wamid.HBgLNjI4MTIzNDU2Nzg5FQIAERgSN0Y3RkIxQ0ZBRkQ5QzY3RkU3AA==",
"kind": "text",
"body": "Thank you, your invoice is ready.",
"occurred_at": "2026-09-18T10:03:00.000Z"
}| Field | Description |
|---|---|
connection_id | A waba connection of your workspace. The key's connection list must allow it. |
to | Optional. The customer's number, digits only, with the country code (8-20 digits). |
to_user_id | Optional. The full BSUID of the customer, for example US.13491208655302741918. Pattern: two uppercase letters, a dot, an optional ENT., then 1-128 letters or digits. |
external_id | The wamid Meta returned to your app. One external_id stores one message. |
kind | text, template, or interactive. The inbox stores interactive as text. |
body | The message text, or a short summary for a template or a form. Up to 16,384 characters. |
occurred_at | Optional ISO 8601 time with offset. Defaults to now. |
Send at least one of to or to_user_id. Use to when you know the phone number, and to_user_id when you do not. Konektor links both to one conversation from the WhatsApp data. Unknown fields are rejected.
A customer who uses a WhatsApp username can have only a BSUID. Example body with a BSUID only:
{
"connection_id": "conn_abc123",
"to_user_id": "US.13491208655302741918",
"external_id": "wamid.HBgLMTM0OTEyMDg2NTUFAgASGBQzQTdFMUIyQzRENUY2QTdCOEM5RDAA",
"kind": "text",
"body": "Thank you, your invoice is ready."
}A new message returns 202 Accepted with { "message_id": "msg_9f2a1b", "inserted": true }. A repeat of the same external_id returns 200 OK with inserted: false. The message reaches your webhooks as inbox.message.sent with actor: null. A rule of a Konektor agent does not run on it.
Mark a conversation read
POST /api/v1/inbox/conversations/{id}/readScope: 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, your own agent:<api_key_id>, or agent:agentic to hand the conversation to Wulan. Another agent's id returns 403 OWNER_FORBIDDEN. Wulan replies to the next customer message. While Wulan auto-reply is off, the answer is 409 AUTO_REPLY_OFF. |
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" }Hand it to Wulan:
{ "owner": "agent:agentic" }Returns { "conversation": { ... } } with the fresh state.
List templates
GET /api/v1/inbox/templatesScope: 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 | AUTO_REPLY_OFF | You handed the conversation to Wulan, but Wulan auto-reply is off. Nothing changed. |
| 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. |
| 409 | DELIVERY_UNCERTAIN | Delivery is uncertain. Check the provider conversation before creating another message. Do not retry automatically. |
| 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 six 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"
}
}inbox.conversation.handover
Fires when the Agentic assistant hands a conversation to your team. The payload has identifiers and a reason code only.
{
"event": "inbox.conversation.handover",
"payload": {
"conversation_id": "7c9ecb52-...",
"reason": "customer_request",
"assigned_to_user_id": "usr_182",
"occurred_at": "2026-10-11T10:05:00.000Z"
}
}reason is one of keyword, max_replies, customer_request, sensitive_topic, upset, no_answer, or other. assigned_to_user_id is null when no team member received the conversation.
inbox.conversation.updated
Fires when a conversation changes: a new status, labels, or assignee, a snooze that ends, or a reply sent from the phone. A webhook receives it only when the webhook has this event selected. The payload has identifiers, the status, and label IDs only. It has no message text, no phone number, and no name.
{
"event": "inbox.conversation.updated",
"payload": {
"conversation_id": "7c9ecb52-...",
"status": "resolved",
"assigned_to_user_id": "usr_182",
"label_ids": ["lbl_3", "lbl_7"],
"occurred_at": "2026-10-11T10:07:00.000Z"
}
}One change can send more than one event, and a retry can send the same event again. Read status and label_ids as the state at the time of delivery, and ignore an event that you already handled.
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.
Wulan answers your questions and sends tutorial videos. Session scheduling is available through Wulan if you still need it.
© 2026 Konektor. All rights reserved.