Webhooks
A webhook is the bridge between Chato and your own software: a website, a warehouse system, 1C, a CRM. It works both ways, and the two directions are independent — you can turn on just one.
- Outgoing — Chato calls you when something happens, and asks you questions during a conversation with a customer.
- Incoming — your software calls Chato: writes to the customer, calls a manager, fixes the contact card, reads the messages.
Set it up in the cabinet: AI assistant → Connections → Webhook.
Outgoing: events sent to your address
Enter the address (https only), optionally a signing secret and your own headers — usually an access key for your software. Then tick what you want to hear about:
| Event | When it fires |
|---|---|
message.received |
a customer wrote |
message.sent |
the customer got a reply — from the assistant or an operator |
conversation.created |
the first message in a new conversation |
conversation.handoff |
the conversation was handed to a manager |
conversation.assigned |
an operator took the conversation |
conversation.reopened |
the conversation was brought back into work |
conversation.closed |
the conversation was closed |
channel.disconnected |
a channel went offline |
Chato sends a POST with a body like this:
{
"workspaceId": "cmr4sk0ya0001o701hl7ghbqk",
"event": "message.received",
"sentAt": "2026-09-08T10:00:00.000Z",
"data": {
"conversationId": "cmt9x1a2b0003o701abcd1234",
"channelId": "cmt9x0zzz0001o701wxyz9876",
"messageId": "cmt9x2c3d0005o701efgh5678",
"direction": "INBOUND",
"type": "TEXT",
"text": "Hi, is my order ready?",
"sentAt": "2026-09-08T10:00:00.000Z",
"isFirstMessage": false
}
}
Reply with 200 — Chato does not parse the response body. If your server stays silent or answers with an error, the conversation with the customer is not held up: the event is simply not delivered, and the failure is recorded in our logs.
Request signature
When a secret is set, Chato puts an HMAC-SHA256 of the request body into the x-chato-signature header, hex-encoded. Verify it on your side to tell our request from someone else's:
const crypto = require('node:crypto');
const expected = crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
const ok = expected === req.headers['x-chato-signature'];
Compute the signature over the raw body, before parsing JSON: a re-serialised object is a different sequence of bytes and the signature will not match.
Calling your software mid-conversation
The same address is used when the assistant reaches a "Call a function" step and asks your software something — "do you have the red one in size 42". The body carries workspaceId, conversationId, sentAt and a data object with whatever the assistant collected. Whatever you answer is what it tells the customer in its own words, so answer with short, plain JSON.
The inbound link: your software acts in a conversation
Press Create link and hand it to your developer. It looks like this:
POST https://chato.kz/api/v1/ai-assistant/inbound/YOUR_KEY
Content-Type: application/json
The key in that link is its only protection. Anyone who has the link can write to your customers. Do not publish it and do not put it in code other people can read. If it leaks, press "Rotate key" — the old one stops working immediately.
Point at a conversation either by its conversationId or by the customer's phone number: any format is accepted — +7 771 525 89 15, 87715258915.
Write to the customer
curl -X POST https://chato.kz/api/v1/ai-assistant/inbound/YOUR_KEY \
-H 'Content-Type: application/json' \
-d '{"phone":"+7 771 525 89 15","text":"Your order is packed, we are open until 7pm"}'
Response: {"status":"sent","conversationId":"..."}.
Attach a file
{
"phone": "77715258915",
"text": "Your invoice",
"fileUrl": "https://your-service.kz/invoice-2026-09.pdf",
"fileName": "September invoice.pdf"
}
The file is fetched from that link (https only, up to 20 MB) and delivered as an attachment. text is optional in that case.
Write first
When there is no conversation with this person yet, add channelId — which channel to write from:
{ "phone": "77715258915", "channelId": "cmt9x0zzz0001o701wxyz9876", "text": "Hello!" }
Without channelId nothing is sent to an unknown number — otherwise a leaked link would turn into a bulk-messaging tool.
Every other action
They all go to the same address; only the action field differs.
action |
What it does | What to send |
|---|---|---|
message |
write to the customer (default) | text and/or fileUrl |
note |
a note for the operator | text |
handoff |
hand over to a manager | text — the reason |
close |
close the conversation | — |
reopen |
bring it back into work | — |
assign |
assign a teammate | operatorEmail |
contact.update |
fix the contact card | contact |
contact.get |
read the contact card | — |
messages.list |
the latest messages | limit |
conversations.list |
the conversation list | status, limit |
deal.create |
create a deal | deal (fields as in the API), contact or phone |
deal.move |
move a deal to a stage | dealId, stageId, for “Lost” — lossReasonId |
deal.note |
add a note to a deal | dealId, text |
About those fields: text — the text (for note it is the operator's note, for handoff the reason, and there it is optional); contact — an object { name, phone }, only what you send is changed; limit — 1 to 100, default 20; status — OPEN, WAITING, IN_PROGRESS or CLOSED. A note never reaches the customer, and after reopen the assistant answers in that conversation again.
Example — hand the conversation to a human:
{ "action": "handoff", "conversationId": "cmt9x1a2b0003o701abcd1234", "text": "customer asked for a manager" }
Example — find out who this is and what state the conversation is in:
{ "action": "contact.get", "phone": "77715258915" }
{
"status": "ok",
"conversationId": "cmt9x1a2b0003o701abcd1234",
"contact": { "id": "...", "name": "Asem", "phone": "77715258915", "username": null },
"conversation": { "id": "...", "status": "OPEN", "channelId": "...", "unreadCount": 2 }
}
conversations.list is the one action that needs no conversation: it is how you find the right one when you have no identifier.
What comes back on an error
| Code | What happened |
|---|---|
404 |
unknown key, or the conversation/channel/teammate is not in your workspace |
400 |
empty text, an unknown action, a too-short phone number, a file over 20 MB |
429 |
too often — the link is limited to 60 requests per minute |
A key only ever works inside your own workspace: someone else's conversationId in the body gets you nothing.
The easiest way to check the wiring: create the link, send contact.get for your own number and confirm your card comes back. It changes nothing and writes nothing to a customer.
Deals and CRM webhooks
The CRM has its own webhook endpoints: with a choice of deal events (deal.created, deal.won, task.completed and more), an HMAC-SHA256 signature with a timestamp, retries on errors, a delivery log and a “Send again” button. The same place issues public API keys with permissions. It is all described in Public deals API and CRM webhooks; the assistant URL described above keeps working as before.



