API

Public deals API and CRM webhooks

Updated September 28, 202610 min read

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 — 429 with a Retry-After header (seconds to wait). The remainder is in X-RateLimit-Remaining. Every key has its own counter: one integration does not eat another's limit.
  • Retry without duplicates — add an Idempotency-Key header to POST (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 with Idempotent-Replayed: true and 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=87015551122 or ?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 2xx answer 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 ping event. 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.

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": "Оплата прошла" }
Public deals API and CRM webhooks — Knowledge base