CRM / API

API altCRM

REST-интерфейс к вашим лидам, воронкам и задачам. Все ответы в JSON, все адреса начинаются с https://api.altcrm.ru/v1.

1. Выпустите ключ

В CRM откройте Организация → API и нажмите «Выпустить ключ». Выберите права: по умолчанию предлагается только чтение. Ключ показывается один раз, сразу после выпуска. Второй раз его не покажет никто, включая нас, — в базе хранится только его отпечаток.

2. Проверьте, что он работает

curl https://api.altcrm.ru/v1/me \
  -H "Authorization: Bearer alt_7f3a9c2b1d_ВАШ_КЛЮЧ"
{
  "organization_id": 2,
  "workspace_id": null,
  "key": { "id": 5, "name": "Интеграция с сайтом", "prefix": "alt_7f3a9c2b1d" },
  "scopes": ["leads.read", "leads.write", "stages.read"]
}

3. Заведите первого лида

curl -X POST https://api.altcrm.ru/v1/leads \
  -H "Authorization: Bearer alt_7f3a9c2b1d_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Иванов Иван",
        "username": "ivanov",
        "stage": "Новый",
        "fields": { "phone": "+79990000000" }
      }'
Публичный API входит в тарифы ALT+ и ALT+ AI. На пробном тарифе ключи выпускаются, но запросы отклоняются с кодом plan_required:api. Тариф виден в CRM: Организация → Подписка.

Авторизация

Ключ передаётся заголовком:

Authorization: Bearer alt_7f3a9c2b1d_L87ZMBCVbjL3oNngrNEHrCqiNpozy7Pv

Если Authorization у вас занят своим прокси или шлюзом, тот же ключ принимается в заголовке X-Api-Key.

Ключ принадлежит организации, а не человеку: он переживает увольнение сотрудника и не зависит от его роли. Ключ можно сузить до одного воркспейса — тогда чужие данные он не отдаст, даже если попросить их явно.

Ключ — это пароль. Он не должен попадать в браузер, мобильное приложение или публичный репозиторий: любой, кто его увидит, получит те же права. Держите ключ на своём сервере. Если ключ утёк — отзовите его в CRM и выпустите новый, старый перестанет работать мгновенно.

Права ключа

Каждый ключ несёт список прав. Права не связаны с ролями сотрудников: роль описывает, что человеку можно делать руками, а право ключа — что можно делать чужой программе.

ПравоЧто открывает
leads.readЧитать лидов и описание полей
leads.writeСоздавать, менять и удалять лидов
stages.readЧитать воркспейсы, воронки и стадии
tags.readЧитать метки
tasks.readЧитать задачи
tasks.writeСоздавать и менять задачи
users.readЧитать сотрудников организации
webhooks.manageРаботать со списком событий вебхуков

Права «на всё» не существует. Если методу нужно право, которого у ключа нет, ответ будет 403 с текстом missing_scope:leads.write.

Ошибки и лимиты

Ошибка всегда приходит одинаковым объектом, а причина — машинным кодом в поле message:

{ "statusCode": 403, "message": "missing_scope:leads.write", "error": "Forbidden" }
КодПричина
401 missing_api_keyЗаголовок с ключом не передан
401 invalid_api_keyКлюч неверный, отозван или истёк
403 plan_required:apiВ тарифе организации нет публичного API
403 missing_scope:…Ключу не выдано нужное право
400 workspace_requiredВ организации несколько воркспейсов, назовите нужный
400 stage_not_foundСтадия не найдена ни по имени, ни по ключу, ни по id
400 unknown_field:кодВ fields передано поле, которого нет в воркспейсе
404 lead_not_foundЛида нет либо он вне области ключа
429 rate_limitedПревышен лимит запросов

Лимит запросов

120 запросов в минуту на ключ. Остаток приходит в заголовках каждого ответа: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (момент сброса, unix-время). При отказе добавляется Retry-After в секундах.

Для регулярной выгрузки не опрашивайте /leads целиком. Передайте updated_since — вернутся только изменившиеся карточки. А чтобы узнавать об изменениях сразу, подпишитесь на вебхуки.

Воркспейсы

Воркспейс — отдельное направление работы со своими воронками, стадиями и метками. Если в организации он один, параметр workspace_id можно не передавать. Если их несколько, а ключ выдан на всю организацию, воркспейс нужно называть явно: угадывать за вас мы не будем.

GET/v1/workspacesstages.read
[
  { "id": 6, "name": "SIMPLE", "color": "#6366f1", "created_at": "2026-08-04T22:21:54.118Z" },
  { "id": 7, "name": "ELECTRONIC", "color": "#0ea5e9", "created_at": "2026-08-04T22:21:54.133Z" }
]

Стадии

GET/v1/stagesstages.read

Параметр: workspace_id (необязателен, см. выше).

[
  {
    "id": "6:a721a4d9d0fb4c98",
    "key": "a721a4d9d0fb4c98",
    "name": "Новый",
    "color": "#6366f1",
    "group": "Основная воронка",
    "sort": 10,
    "outcome": "in_progress",
    "workspace_id": 6
  }
]

outcome различает рабочие стадии и итоговые: in_progress, won, lost. По нему считают конверсию, не разбирая названия.

Стадию можно назвать тремя способами. Везде, где принимается stage, подойдёт человеческое имя ("Новый"), короткий ключ ("a721a4d9d0fb4c98") или полный id ("6:a721a4d9d0fb4c98"). Переводить одно в другое на своей стороне не нужно.

Метки

GET/v1/tagstags.read
[ { "id": 3, "name": "Горячий", "color": "#ef4444", "workspace_id": 6 } ]

Поля лида

Кроме имени и ника у лида есть кастомные поля, которые организация заводит сама: телефон, почта, ИНН, что угодно. Снаружи они адресуются машинным именем (key), а не номером: номер меняется при переносе, имя — нет.

GET/v1/fieldsleads.read
[
  { "key": "phone", "label": "Телефон", "type": "text", "options": [], "system_key": "phone", "workspace_id": 6 },
  { "key": "budget", "label": "Бюджет", "type": "number", "options": [], "system_key": "", "workspace_id": 6 }
]

Непустой system_key означает, что поле завела платформа: его нельзя удалить и нельзя сменить ему тип.

Сотрудники

GET/v1/usersusers.read

Их id ставят владельцем лида и исполнителем задачи.

[ { "id": 5, "name": "Пётр Смирнов", "username": "smirnov", "is_active": true } ]

Лиды

GET/v1/leadsleads.read
ПараметрЧто делает
workspace_idВоркспейс
stageИмя, короткий ключ или полный id стадии
groupВоронка
tag_idТолько с этой меткой
owner_idТолько этого сотрудника
queryПоиск по имени, нику и ФИО
updated_sinceISO-8601: изменённые с этого момента
limitРазмер страницы, до 200 (по умолчанию 50)
offsetСдвиг
{
  "total": 348,
  "limit": 50,
  "offset": 0,
  "items": [ { "id": 1042, "name": "Иванов Иван", "…": "…" } ]
}
GET/v1/leads/{id}leads.read
{
  "id": 1042,
  "workspace_id": 6,
  "name": "Иванов Иван",
  "display_name": "Иван",
  "username": "ivanov",
  "telegram_user_id": 123456789,
  "stage": {
    "id": "6:a721a4d9d0fb4c98",
    "key": "a721a4d9d0fb4c98",
    "name": "Новый",
    "group": "Основная воронка",
    "outcome": "in_progress"
  },
  "tags": [ { "id": 3, "name": "Горячий", "color": "#ef4444" } ],
  "owner": { "id": 5, "name": "Пётр Смирнов", "username": "smirnov" },
  "note": "Просил перезвонить после обеда",
  "due_date": "2026-08-20",
  "fields": { "phone": "+79990000000", "budget": "150000" },
  "created_at": "2026-08-01T09:14:02.100Z",
  "updated_at": "2026-08-07T18:20:41.980Z",
  "stage_changed_at": "2026-08-05T11:02:00.000Z"
}

Лида, которого нет, и лида из чужого воркспейса не отличить: оба дают 404 lead_not_found. Так чужой номер не подтверждает, что такой лид существует.

POST/v1/leadsleads.write

Достаточно имени или ника. Без указания стадии лид попадает в первую стадию воронки.

{
  "workspace_id": 6,
  "stage": "Новый",
  "name": "Иванов Иван",
  "username": "ivanov",
  "telegram_user_id": 123456789,
  "note": "Заявка с сайта",
  "tag_ids": [3],
  "owner_id": 5,
  "fields": { "phone": "+79990000000" }
}

В ответ приходит созданный лид целиком. Подписчикам уходит событие lead.created.

PATCH/v1/leads/{id}leads.write

Меняются только переданные поля. Остальные остаются как были.

{ "stage": "В работе", "owner_id": 5, "fields": { "budget": "200000" } }

Смена стадии проходит через те же автоматизации, что и перенос карточки руками, и попадает в журнал действий с именем ключа. Кроме lead.updated уходит отдельное событие lead.stage_changed.

Чтобы снять владельца, передайте "owner_id": null. Чтобы очистить поле — пустую строку.

DELETE/v1/leads/{id}leads.write

Необратимо. В журнале действий остаётся запись о том, что лида удалил ключ, и какой именно.

{ "ok": true }

Задачи

GET/v1/taskstasks.read

Параметры: status, limit, offset. Формат страницы такой же, как у лидов.

POST/v1/taskstasks.write
{ "title": "Перезвонить Иванову", "priority": "high", "assignee_ids": [5] }
PATCH/v1/tasks/{id}tasks.write
{ "status": "done" }

Перевод в done порождает событие task.completed.

Вебхуки

Обратное направление: CRM сама шлёт POST на ваш адрес, когда что-то произошло. Адрес добавляют в CRM: Организация → API → Вебхуки. Принимается только https.

СобытиеКогда приходит
lead.createdЛид создан
lead.updatedЛид изменён
lead.stage_changedЛид сменил стадию
lead.deletedЛид удалён
task.createdЗадача создана
task.completedЗадача выполнена

Тело запроса:

{
  "event": "lead.stage_changed",
  "sent_at": "2026-08-07T20:31:04.512Z",
  "organization_id": 2,
  "data": {
    "lead": { "id": 1042, "…": "…" },
    "from": { "key": "a721a4d9d0fb4c98", "name": "Новый" }
  }
}

Заголовки

ЗаголовокЗначение
X-AltCRM-EventНазвание события
X-AltCRM-DeliveryНомер доставки, уникальный
X-AltCRM-TimestampUnix-время отправки
X-AltCRM-Signaturesha256=…, см. ниже

Повторы

Успехом считается любой ответ 2xx. Всё остальное, включая молчание, — отказ, и тогда доставка повторяется пять раз с растущей паузой: через минуту, 5 минут, 25 минут, 2 часа и 10 часов. После пятой попытки доставка помечается неудачной, а история попыток остаётся видна в CRM.

Отвечайте 200 сразу, а работу делайте после. Мы ждём ответа 8 секунд; обработка внутри этого времени превращает медленную задачу в недоставленное событие. Один и тот же X-AltCRM-Delivery может прийти дважды — считайте его ключом и пропускайте повторы.

Проверка подписи

Подписывается строка <timestamp>.<тело как есть> алгоритмом HMAC-SHA256 на секрете вебхука. Секрет виден в CRM рядом с адресом.

Метка времени входит в подпись намеренно: без неё перехваченный запрос можно повторять вечно, и подпись оставалась бы верной. Отвергайте запросы старше пяти минут.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto"

function verify(secret, rawBody, headers) {
  const timestamp = headers["x-altcrm-timestamp"]
  const got = String(headers["x-altcrm-signature"] || "").replace(/^sha256=/, "")
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const want = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex")
  const a = Buffer.from(want), b = Buffer.from(got)
  return a.length === b.length && timingSafeEqual(a, b)
}

Важно: подписывается сырое тело. Если фреймворк уже разобрал JSON, повторная сборка через JSON.stringify даст другую строку и подпись не сойдётся.

PHP

$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_ALTCRM_TIMESTAMP'] ?? '';
$got = str_replace('sha256=', '', $_SERVER['HTTP_X_ALTCRM_SIGNATURE'] ?? '');

if (abs(time() - (int)$ts) > 300) { http_response_code(400); exit; }

$want = hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($want, $got)) { http_response_code(401); exit; }

http_response_code(200);

Python

import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, timestamp: str, signature: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    want = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(want, signature.removeprefix("sha256="))