Public deals API and CRM webhooks
You can run Chato deals from your own software: the website creates a deal from a request, 1C closes it after payment, the warehouse sets a task for a manager. In the other direction, Chato tells your software what happened in the CRM: a deal moved, was won, got a new responsible person.
Set it up in the cabinet: Deals → ⋯ → Settings → API & webhooks (visible to admins and the owner). The same tab links to the full request reference — Swagger: https://chato.kz/api/v1/public/docs, and the OpenAPI description — https://chato.kz/api/v1/public/openapi.json.
API key
Click New key, give it a name (“Website”, “1C”) and tick its permissions:
| Permission | Allows |
|---|---|
deals:read |
read deals and their notes |
deals:write |
create, change, move, close and delete deals, add notes |
contacts:read / contacts:write |
find / create and change contacts |
tasks:read / tasks:write |
read / create and complete tasks |
pipelines:read |
pipelines, stages, custom fields, loss reasons |
users:read |
the team list (for responsibleUserId) |
messages:send |
message clients |
The key is shown once — copy it right away. Chato stores only its fingerprint and cannot show the key again. The list shows who issued each key and when it was last used. Revoke — the key stops working immediately, your software gets 401.
Treat the key like a password: keep it on the server, in environment variables. Never put it into website code that runs in a visitor's browser.
Everything your software does with the key is recorded in the deal history on behalf of whoever issued the key, marked “via API”. A key cannot do more than its author: if a pipeline is hidden from that employee, it is hidden from the key too. Stage automations, notifications and live board updates for colleagues work exactly as they do in the cabinet.
Making requests
https://chato.kz/api/v1/public
Authorization: Bearer chato_sk_...
Content-Type: application/json
You can also pass the key in the X-Api-Key header. A successful response is always in the data field.
- Limit — 120 requests per minute per key. Above that —
429with aRetry-Afterheader (seconds to wait). The remainder is inX-RateLimit-Remaining. Every key has its own counter: one integration does not eat another's limit. - Retry without duplicates — add an
Idempotency-Keyheader toPOST(any string up to 255 characters, e.g. your own request id). A repeat with the same key within 24 hours returns the first response withIdempotent-Replayed: trueand does not create a second deal. The same key with a different body —422 IDEMPOTENCY_KEY_REUSED.
Errors
Errors always look the same (message is in Russian; translate by textCode):
{
"error": {
"code": "REQUIRED_FIELDS",
"message": "Заполните поля, чтобы перенести в «Договор подписан»",
"textCode": "REQUIRED_FIELDS",
"params": { "stage": "Договор подписан" },
"details": { "missing": [{ "key": "data_dogovora", "label": "Дата договора", "type": "DATE" }] }
},
"statusCode": 422
}
| Code | When |
|---|---|
401 UNAUTHORIZED |
no key, wrong key or revoked |
403 FORBIDDEN_SCOPE |
the key lacks the required permission |
404 DEAL_NOT_FOUND etc. |
no such object in your company (other companies' ids always give 404) |
400 VALIDATION_FAILED |
invalid data, the list is in details.validation |
422 REQUIRED_FIELDS |
the stage needs required fields — which ones, see details.missing |
422 LOSS_REASON_REQUIRED |
moving to “Lost” without a reason while the pipeline requires one |
429 RATE_LIMITED |
limit exceeded, see Retry-After |
Reference data
curl https://chato.kz/api/v1/public/pipelines -H "Authorization: Bearer $CHATO_KEY"
Pipelines with stages (kind: OPEN — working, WON — “Won”, LOST — “Lost”). Also:
GET /deal-fields— custom fields:key(the field code used to write values), type, options, on which stages it is required;GET /loss-reasons— loss reasons;GET /users— team members (id, name, e-mail, role).
Deals
Create
curl -X POST https://chato.kz/api/v1/public/deals \
-H "Authorization: Bearer $CHATO_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: site-order-10542' \
-d '{
"pipelineId": "PIPELINE_ID",
"title": "Заявка с сайта",
"amount": 150000,
"contact": { "name": "Айгерим", "phone": "+7 701 555 11 22", "email": "aigerim@example.com" },
"tags": ["сайт"],
"fields": { "gorod": "almaty", "obem": 40 }
}'
The contact is either contactId or contact. With contact, Chato first looks the person up: by phone (last 10 digits, any format), then by e-mail (case-insensitive); if nobody is found, it creates a contact. contactCreated in the response tells you which happened. Without pipelineId and stageId the deal lands in the first stage of the first pipeline. The default responsible person is the key's author; "responsibleUserId": null — no one. The deal source is “API”.
Custom fields go in the fields object by the codes from /deal-fields: an option id for a list, an array for multi-select, 2026-09-28 for a date, true/false for a checkbox. An invalid value — 400 naming the field.
Change
curl -X PATCH https://chato.kz/api/v1/public/deals/DEAL_ID \
-H "Authorization: Bearer $CHATO_KEY" -H 'Content-Type: application/json' \
-d '{"amount": 180000, "tags": ["сайт", "vip"], "fields": {"oplata": "invoice"}}'
Only what you send changes. tags replace the previous list, null in a field clears it.
Move, close
# to a stage — same rules as on the board
curl -X POST https://chato.kz/api/v1/public/deals/DEAL_ID/move \
-H "Authorization: Bearer $CHATO_KEY" -H 'Content-Type: application/json' \
-d '{"stageId": "STAGE_ID", "fields": {"data_dogovora": "2026-09-28"}}'
# won
curl -X POST https://chato.kz/api/v1/public/deals/DEAL_ID/close \
-H "Authorization: Bearer $CHATO_KEY" -H 'Content-Type: application/json' \
-d '{"result": "won"}'
# lost — with a reason from /loss-reasons
curl -X POST https://chato.kz/api/v1/public/deals/DEAL_ID/close \
-H "Authorization: Bearer $CHATO_KEY" -H 'Content-Type: application/json' \
-d '{"result": "lost", "lossReasonId": "REASON_ID", "lossComment": "выбрали другого"}'
If the stage has required fields that are empty in the deal, you get 422 REQUIRED_FIELDS with the list — repeat the request with them in fields. close picks the first “Won” / “Lost” stage of the deal's pipeline; to use another one, pass its stageId.
Find, read, delete
curl 'https://chato.kz/api/v1/public/deals?pipelineId=PIPELINE_ID&status=open&limit=50' \
-H "Authorization: Bearer $CHATO_KEY"
Response: {"data": {"items": [...], "nextCursor": "..."}}. The next page is the same request with &cursor=<nextCursor>; nextCursor: null — no more pages. Filters: pipelineId, stageId, status (open, won, lost, closed, unsorted), responsibleUserId (or none), contactId, tag, search (title, client name or number), updatedSince and createdSince (ISO date — handy for “what changed since last sync”).
GET /deals/DEAL_ID— one deal;DELETE /deals/DEAL_ID— to the trash for 30 days (204);POST /deals/DEAL_ID/restore— bring it back.
Notes and tasks
curl -X POST https://chato.kz/api/v1/public/deals/DEAL_ID/notes \
-H "Authorization: Bearer $CHATO_KEY" -H 'Content-Type: application/json' \
-d '{"text": "Оплата прошла в 1С"}'
curl -X POST https://chato.kz/api/v1/public/deals/DEAL_ID/tasks \
-H "Authorization: Bearer $CHATO_KEY" -H 'Content-Type: application/json' \
-d '{"kind": "CALL", "text": "Перезвонить", "dueAt": "2026-10-01T09:00:00+05:00"}'
curl -X POST https://chato.kz/api/v1/public/tasks/TASK_ID/complete \
-H "Authorization: Bearer $CHATO_KEY" -H 'Content-Type: application/json' \
-d '{"result": "Дозвонились"}'
GET /deals/DEAL_ID/notes and GET /deals/DEAL_ID/tasks — lists. Task kinds: CALL, MEETING, WRITE, OTHER and your own (CUSTOM + customTypeId).
Contacts
GET /contacts?phone=87015551122or?email=...— find (up to 20);POST /contacts{name, phone, email}— create; if the person already exists, you get them back (200,"created": false), a new one —201;PATCH /contacts/CONTACT_ID— change name, phone, e-mail.
Message a client
With messages:send: POST /messages with conversationId or phone (for a new number also channelId), text and/or fileUrl — the same as the message action of the inbound link.
Webhooks: deal events to your URL
Add endpoint → enter an https URL and tick the events. After saving, the endpoint gets its own signing secret (the “Show” button).
| Event | When |
|---|---|
deal.created |
a deal was created (in the cabinet, via API, from an inquiry) |
deal.updated |
title, amount, tags, fields or responsible changed |
deal.stage_changed |
the deal moved to another stage |
deal.won / deal.lost |
the deal was won / lost |
deal.deleted / deal.restored |
to the trash / back from it |
deal.responsible_changed |
the responsible person changed |
task.created / task.completed |
a task was created / completed |
note.created |
a note was added |
The same endpoint can also receive chat events (message.received, conversation.closed and others — see Webhooks).
Chato sends a POST:
{
"id": "evt_3f2a9c...",
"event": "deal.won",
"createdAt": "2026-09-28T10:15:00.000Z",
"workspaceId": "cmr4sk0ya0001o701hl7ghbqk",
"data": {
"deal": {
"id": "cmulms3cp000r29eujik2dr1e",
"title": "Заявка с сайта",
"amount": 150000,
"currency": "KZT",
"status": "won",
"pipeline": { "id": "...", "name": "Продажи" },
"stage": { "id": "...", "name": "Успешно", "kind": "WON" },
"contact": { "id": "...", "name": "Айгерим", "phone": "77015551122", "email": "aigerim@example.com" },
"responsible": { "id": "...", "name": "Асхат", "email": "askhat@example.com" },
"tags": ["сайт"],
"fields": { "gorod": "almaty" },
"closedAt": "2026-09-28T10:15:00.000Z"
},
"changes": {
"stage": {
"from": { "id": "...", "name": "Счёт выставлен", "kind": "OPEN" },
"to": { "id": "...", "name": "Успешно", "kind": "WON" }
}
},
"actor": { "type": "api", "userId": "..." }
}
}
deal is a snapshot of the deal in the same shape the API returns. changes — what changed (from → to; custom fields as fields.<code>). actor.type: user (an employee in the cabinet), api, webhook, automation, system. For task.* and note.created, task / note sit next to deal.
Headers: X-Chato-Event (the event), X-Chato-Event-Id (same on retries — use it to drop duplicates), X-Chato-Delivery, X-Chato-Timestamp, X-Chato-Signature.
Verifying the signature
X-Chato-Signature is sha256= + HMAC-SHA256 with the endpoint secret over the string <X-Chato-Timestamp>.<raw body>. Compute it over the raw body, before parsing JSON, and reject requests older than 5 minutes.
Node.js (Express):
const crypto = require('node:crypto');
app.post('/chato', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Chato-Timestamp');
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.CHATO_WEBHOOK_SECRET)
.update(`${ts}.${req.body}`)
.digest('hex');
const given = req.get('X-Chato-Signature') || '';
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!fresh || given.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
res.sendStatus(200); // answer at once, heavy work in the background
});
PHP:
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_CHATO_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, getenv('CHATO_WEBHOOK_SECRET'));
$given = $_SERVER['HTTP_X_CHATO_SIGNATURE'] ?? '';
if (abs(time() - (int) $ts) > 300 || !hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(200);
Python (Flask):
import hmac, hashlib, os, time
from flask import request, abort
@app.post("/chato")
def chato():
body = request.get_data() # raw bytes
ts = request.headers.get("X-Chato-Timestamp", "")
expected = "sha256=" + hmac.new(
os.environ["CHATO_WEBHOOK_SECRET"].encode(), f"{ts}.".encode() + body, hashlib.sha256
).hexdigest()
given = request.headers.get("X-Chato-Signature", "")
if abs(time.time() - int(ts or 0)) > 300 or not hmac.compare_digest(expected, given):
abort(401)
event = request.get_json()
return "", 200
Retries, log, auto-disable
- Success is any
2xxanswer within 10 seconds. Otherwise Chato retries after 1 min, 5 min, 30 min, 2 h and 6 h — six attempts in total. Redirects (3xx) are not followed. - The delivery log under the endpoint: status, response code, attempts, what was sent and what your server answered. Send again re-delivers with the same event
id. The log is kept for 30 days. - If deliveries keep failing (25 attempts in a row), the endpoint turns itself off and the owner and admins get a notification. Fix the receiver and click Turn on.
- Test sends a
pingevent. New secret — the old one stops matching immediately.
Answer 200 right away and do the processing in the background: a slow answer counts as a failure and gets retried — and a retry arrives with the same X-Chato-Event-Id, so it is easy to recognise.
Deals through the inbound link
If you already use the assistant's inbound link, it can handle deals too — with the same rules as the API and a “via webhook” mark in the history:
{ "action": "deal.create", "contact": { "name": "Айгерим", "phone": "+77015551122" }, "deal": { "title": "Заказ из 1С", "amount": 77000 } }
{ "action": "deal.move", "dealId": "DEAL_ID", "stageId": "STAGE_ID" }
{ "action": "deal.note", "dealId": "DEAL_ID", "text": "Оплата прошла" }



