Custom Inbox
Connect your own product, app, or automation to Kronos. You send inbound customer messages to the inbox endpoint, and Kronos sends replies back to your webhook.
How to connect it
Create the Custom Inbox
Open Kronos, go to Connect, then open the Inboxes section and choose Custom Inbox.
Add your webhook URL
2.1. Enter the inbox name customers or your team will recognize.
2.2. Paste the HTTPS webhook URL that should receive Kronos replies.
Choose webhook events
3.1. Keep message.created enabled for replies.
3.2. Enable agent.tool.called if your app should show tool activity.
3.3. Enable agent.reasoning.created only if you want reasoning from real reasoning models.
Copy the credentials
4.1. Copy the inbound endpoint. Your app will send customer messages there.
4.2. Copy the webhook signing secret. It is shown once, and you can rotate it later from inbox settings.
Send a test message
Create an API key with custom_inbox:write, send one inbound message to the endpoint, then confirm your webhook receives the reply or a webhook.test event.
What you need
- An HTTPS webhook URL that accepts POST requests.
- An API key from the same Kronos account as the Custom Inbox, with the
custom_inbox:writescope. - Your own backend route that keeps the API key server-side. Do not call Kronos directly from browser code.
- A stable user id from your system for
external_user_id. - A stable thread id for
external_conversation_id, or reuse the generated one returned by Kronos. - An AI agent or workflow assigned to the Custom Inbox if you expect automatic replies.
Choose a session pattern
Custom Inbox does not require a specific frontend. The important part is how your app maps its visitor, user, or thread ids to Kronos fields.
Temporary web chat
Good for landing-page chat bubbles, demos, and tests where the conversation can live only in the current browser.
- 1. Generate a random visitor id in the browser and use it as
external_user_id. - 2. Omit
external_conversation_idon the first message, then store the value Kronos returns inlocalStorageorsessionStorage. - 3. Send browser messages to your backend, and let your backend call the Kronos inbound endpoint with the API key.
- 4. Receive webhook replies on your backend and expose them to the browser with polling, SSE, WebSocket, or your own temporary store.
Persistent product chat
Use this for signed-in apps, support inboxes, order threads, and conversations that must survive refreshes, devices, and deploys.
- 1. Use your database user/contact id as
external_user_id. - 2. Use your durable ticket, conversation, cart, order, or room id as
external_conversation_id. - 3. Store inbound and webhook messages in your database, deduping by
message_id,event_id, or webhook message id. - 4. Hydrate the chat UI from your database and update it when new webhook events arrive.
Minimum backend flow
The API key belongs on your server. A typical web or app integration uses this flow:
- 1. Your frontend sends the visitor message to your backend route.
- 2. Your backend calls the Kronos inbound endpoint with
Authorization: Bearer $KRONOS_API_KEY. - 3. Your backend stores or returns the
external_conversation_idso the next message stays in the same thread. - 4. Kronos sends the AI reply to your webhook as an async
message.createdevent. - 5. Your webhook validates the signature, stores or emits the reply, then your UI shows it by polling, SSE, WebSocket, or your own realtime layer.
Send an inbound message
Use the inbound endpoint shown after you connect the inbox. Each inbound message needs a stable message_id so retries do not create duplicates. content accepts 1-4096 characters, and metadata can contain up to 10KB and 50 top-level keys.
curl -X POST https://api.kronos.com.mx/v1/custom-inboxes/<integration_id>/messages \
-H "Authorization: Bearer $KRONOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "user_123",
"external_conversation_id": "thread_abc123",
"message_id": "msg_0001",
"content": "Hola, quiero revisar mi pedido",
"sender": { "name": "Maria" },
"metadata": { "source": "mobile_app" }
}'To intentionally start a fresh Kronos thread even when you already have an old thread id, send "new_conversation": true. Kronos will return the new external_conversation_id; store that value if the new thread should continue.
Accepted response
The endpoint returns once the message is accepted. Agent replies are generated asynchronously and sent separately to your webhook. A successful accepted response means Kronos stored the inbound message; it does not mean the AI reply has already been delivered. New accepted messages return HTTP 202. Idempotent replays may return HTTP 200 with X-Idempotent-Replay: true.
{
"status": "accepted",
"integration_id": "4ad91e83-7d48-4371-9f2f-4505c7912c2e",
"conversation_id": "b8e547b4-3837-49d2-9f4b-6c2e9d01fd7d",
"external_conversation_id": "thread_abc123",
"inbound_message_id": "99618f3e-4f7a-4e17-896e-250f6a92db31",
"idempotent_replay": false,
"created_at": "2026-07-06T09:12:27.852Z"
}Receive replies by webhook
Your webhook receives one POST per enabled event. For normal replies, expect one message.created event per message bubble. Validate the signature against the exact raw request body before trusting the event.
X-Kronos-Event-Id: <uuid>
X-Kronos-Timestamp: <unix_seconds>
X-Kronos-Signature: v1=<hex_hmac_sha256(timestamp + "." + raw_body)>{
"event_id": "5bdbb9f1-21e6-4f90-984f-67172d0e7e9e",
"event_type": "message.created",
"created_at": "2026-07-06T09:12:32.102Z",
"data": {
"integration_id": "4ad91e83-7d48-4371-9f2f-4505c7912c2e",
"conversation_id": "b8e547b4-3837-49d2-9f4b-6c2e9d01fd7d",
"external_conversation_id": "thread_abc123",
"external_user_id": "user_123",
"message": {
"id": "0f6c052c-4c95-43ab-8d5a-569b7a7184e7",
"sender": "agent",
"content": "Claro, lo reviso contigo.",
"created_at": "2026-07-06T09:12:32.101Z"
}
}
}Your receiver should return any 2xx status quickly. Store the event_idor webhook message id before updating your UI so duplicate deliveries or manual tests do not create duplicate bubbles.
Events you can enable
Sent for each outbound reply bubble.
Sent only when the selected model is a real reasoning model.
Sent when the agent uses a tool. The payload includes the user-facing tool name.
Sent when you click Test webhook from the inbox settings page.
Quick checklist
- 1. Create the Custom Inbox and copy the inbound endpoint.
- 2. Store the signing secret somewhere safe.
- 3. Create an API key with
custom_inbox:writefrom the same Kronos account that owns this Custom Inbox. - 4. Assign an AI agent or workflow to the inbox if you expect automatic replies.
- 5. Send one test message to the inbound endpoint from your backend, not from browser code.
- 6. Click Test webhook in inbox settings and confirm your receiver gets the event.
- 7. Confirm your UI can show an async
message.createdwebhook reply for the sameexternal_conversation_id.
Connection FAQ
What if I close the setup before copying the signing secret?
Open the inbox settings and rotate the secret. Copy the new value and update your webhook receiver.
What should I use as the conversation id?
Use your own stable thread id as external_conversation_id. If you do not have one, omit it on the first message and reuse the value Kronos returns.
Can I build a landing-page chat without storing conversations forever?
Yes. Generate browser-scoped ids and keep the returned external_conversation_idin local or session storage. For production-grade persistence across devices or deploys, store the ids and webhook messages in your backend database.
Why did the API accept my message but no reply appeared?
Check that the Custom Inbox is assigned to an AI agent or workflow, that message.createdis enabled, and that your webhook receiver returns a quick 2xx response after validating the signature.
Why do I get integration_not_found or HTTP 404?
The API key is valid, but it cannot access that Custom Inbox. Use the inbound endpoint and an API key from the same Kronos account, and make sure the key includes custom_inbox:write.
How do I confirm the webhook is working?
Click Test webhook from the inbox settings page and check that your receiver gets a webhook.test event.