API

Webhooks

Updated September 28, 20266 min read

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.

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.