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" }
}'
plan_required:api. Тариф виден в CRM: Организация → Подписка.
Авторизация
Ключ передаётся заголовком:
Authorization: Bearer alt_7f3a9c2b1d_L87ZMBCVbjL3oNngrNEHrCqiNpozy7Pv
Если Authorization у вас занят своим прокси или шлюзом, тот же ключ принимается
в заголовке X-Api-Key.
Ключ принадлежит организации, а не человеку: он переживает увольнение сотрудника и не зависит от его роли. Ключ можно сузить до одного воркспейса — тогда чужие данные он не отдаст, даже если попросить их явно.
Права ключа
Каждый ключ несёт список прав. Права не связаны с ролями сотрудников: роль описывает, что человеку можно делать руками, а право ключа — что можно делать чужой программе.
| Право | Что открывает |
|---|---|
leads.read | Читать лидов и описание полей |
leads.write | Создавать, менять и удалять лидов |
stages.read | Читать воркспейсы, воронки и стадии |
tags.read | Читать метки |
tasks.read | Читать задачи |
tasks.write | Создавать и менять задачи |
users.read | Читать сотрудников организации |
finance.read | Читать операции, своды и категории |
finance.write | Заводить, менять и удалять операции |
templates.read | Читать шаблоны воркспейса |
templates.write | Создавать, менять и удалять шаблоны |
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 можно не передавать. Если их
несколько, а ключ выдан на всю организацию, воркспейс нужно называть явно: угадывать за вас
мы не будем.
[
{ "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" }
]
Стадии
Параметр: 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").
Переводить одно в другое на своей стороне не нужно.
Поля лида
Кроме имени и ника у лида есть кастомные поля, которые организация заводит сама: телефон,
почта, ИНН, что угодно. Снаружи они адресуются машинным именем
(key), а не номером: номер меняется при переносе, имя — нет.
[
{ "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 означает, что поле завела платформа: его нельзя удалить
и нельзя сменить ему тип.
Сотрудники
Их id ставят владельцем лида и исполнителем задачи.
[ { "id": 5, "name": "Пётр Смирнов", "username": "smirnov", "is_active": true } ]
Аккаунты Telegram
Номера, подключённые к воркспейсу. Их id ставят лиду в
telegram_account_id при создании.
[ { "id": 25, "label": "Анатолий", "phone": "79935716560", "username": "anatoly", "status": "ready", "workspace_id": 10 } ]
Лиды
| Параметр | Что делает |
|---|---|
workspace_id | Воркспейс |
stage | Имя, короткий ключ или полный id стадии |
group | Воронка |
tag_id | Только с этой меткой |
owner_id | Только этого сотрудника |
query | Поиск по имени, нику и ФИО |
updated_since | ISO-8601: изменённые с этого момента |
limit | Размер страницы, до 200 (по умолчанию 50) |
offset | Сдвиг |
{
"total": 348,
"limit": 50,
"offset": 0,
"items": [ { "id": 1042, "name": "Иванов Иван", "…": "…" } ]
}
{
"id": 1042,
"workspace_id": 6,
"name": "Иванов Иван",
"display_name": "Иван",
"username": "ivanov",
"telegram_user_id": 123456789,
"telegram_account_id": 25,
"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. Так чужой номер не подтверждает, что такой лид
существует.
Достаточно имени или ника. Без указания стадии лид попадает в первую стадию воронки.
Указывайте telegram_account_id — номер, с которого с
человеком будут переписываться. Список номеров отдаёт
GET /v1/telegram-accounts.
Лид без номера не связан с Telegram ничем. В списке чатов он не помечен как заведённый, при открытии чата CRM предложит завести его заново (и получится вторая карточка на того же человека), а входящие и исходящие сообщения в карточку не попадут. Привязывать такого лида придётся руками, по одному.
telegram_user_id при этом знать не нужно: числовой id подставится
сам, когда оператор откроет чат с этим ником. Если id уже известен, передайте
и его: связь установится сразу, не дожидаясь первого открытия.
{
"workspace_id": 6,
"stage": "Новый",
"name": "Иванов Иван",
"username": "ivanov",
"telegram_account_id": 25,
"telegram_user_id": 123456789,
"note": "Заявка с сайта",
"tag_ids": [3],
"owner_id": 5,
"fields": { "phone": "+79990000000" }
}
В ответ приходит созданный лид целиком. Подписчикам уходит событие lead.created.
Меняются только переданные поля. Остальные остаются как были.
{ "stage": "В работе", "owner_id": 5, "fields": { "budget": "200000" } }
Смена стадии проходит через те же автоматизации, что и перенос карточки руками, и
попадает в журнал действий с именем ключа. Кроме lead.updated уходит
отдельное событие lead.stage_changed.
Чтобы снять владельца, передайте "owner_id": null. Чтобы очистить поле — пустую строку.
telegram_account_id здесь же: им доставляют номер лидам, заведённым
без него, и меняют номер, если человека передали другому оператору.
null отвязывает.
Необратимо. В журнале действий остаётся запись о том, что лида удалил ключ, и какой именно.
{ "ok": true }
Задачи
Параметры: status, limit, offset. Формат страницы такой же, как у лидов.
{ "title": "Перезвонить Иванову", "priority": "high", "assignee_ids": [5] }
{ "status": "done" }
Перевод в done порождает событие task.completed.
Бухгалтерия
Операции принадлежат организации, а не воркспейсу: ключ, сужённый до
воркспейса, всё равно видит их все. Так устроена и сама CRM — «купили прокси» это законная
операция, не относящаяся ни к какому направлению, и граница по воркспейсу разрезала бы свод
по живому. Нужен разрез — задайте его фильтром workspace_id, а значением
none получите операции, не привязанные ни к одному.
Суммы — в копейках и всегда положительные: направление задаёт
kind, а не знак. Дробные рубли в числе с плавающей точкой превращаются в
3 999.9999, поэтому здесь целое.
Фильтры: from, to (YYYY-MM-DD, включительно), kind
(expense или income), category, lead_id,
executor_id, workspace_id, q — поиск по подписи,
заметке, категории и имени лида.
{
"items": [
{
"id": 318, "kind": "expense", "amount_cents": 400000,
"occurred_on": "2026-08-19", "title": "Реклама в Телеграме",
"category": "Маркетинг", "lead_id": 1042, "lead_title": "Иванов Иван",
"executor_id": null, "created_by": null
}
],
"totals": { "income": 0, "expense": 400000, "net": -400000, "count": 1 }
}
totals считаются по этой же выборке — иначе сумма не сошлась бы с тем, что
вернулось. lead_title хранится рядом с lead_id: лидов удаляют,
а история трат должна остаться читаемой.
Те же фильтры, три свода: by_category, by_executor, by_lead.
Заведённые категории, частые сверху. Категория — свободная строка, но повторять существующие стоит точно.
{ "kind": "expense", "amount_cents": 400000, "title": "Реклама в Телеграме",
"category": "Маркетинг", "occurred_on": "2026-08-19", "lead_id": 1042 }
Обязательны только amount_cents и title. Привязали к лиду —
его имя снимется в lead_title, а воркспейс операции возьмётся у него же.
created_by у операций, заведённых ключом, остаётся пустым: их создал не
человек, и приписывать действие кому-то из сотрудников было бы неправдой.
{ "amount_cents": 450000, "category": "Реклама" }Отвечает { "ok": true }.
Шаблоны
Заготовки сообщений. В отличие от бухгалтерии принадлежат воркспейсу — как
стадии и метки, — поэтому ключ на всю организацию должен называть workspace_id,
если воркспейсов несколько.
Дерево отдаётся плоским списком с parent_id: предела вложенности у него нет, и
собирать его на сервере в чужой формат незачем. Кроме шаблонов и папок в нём встречаются
разделители — черта с подписью, is_separator.
[
{ "id": 12, "parent_id": null, "is_folder": true, "name": "Первый контакт" },
{ "id": 13, "parent_id": 12, "is_folder": false, "name": "Приветствие",
"text": "Здравствуйте! Меня зовут…", "attachments": [], "ref_msg_ids": null }
]
{ "name": "Приветствие", "text": "Здравствуйте! Меня зовут…", "parent_id": 12 }
Папка — is_folder: true, разделитель — is_separator: true
(с папкой не сочетается: внутрь черты ничего не кладут).
Вложения и привязка к сообщениям Telegram-хранилища снаружи не заводятся. Дело не в осторожности: файл живёт в супергруппе Telegram, и положить его туда может только юзербот с сессией аккаунта. Ссылка на сообщение, которого никто не отправлял, дала бы битый шаблон, и обнаружилось бы это в момент отправки клиенту.
{ "name": "Приветствие (короткое)", "text": "Здравствуйте!" }
Текст папки и шаблона, собранного из сообщений хранилища, не меняется: его источник лежит в Telegram.
У папки уносит всё поддерево — так же, как в интерфейсе.
Вебхуки
Обратное направление: 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-Timestamp | Unix-время отправки |
X-AltCRM-Signature | sha256=…, см. ниже |
Повторы
Успехом считается любой ответ 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="))