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 | Читать сотрудников организации |
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 } ]
Лиды
| Параметр | Что делает |
|---|---|
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,
"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. Так чужой номер не подтверждает, что такой лид
существует.
Достаточно имени или ника. Без указания стадии лид попадает в первую стадию воронки.
{
"workspace_id": 6,
"stage": "Новый",
"name": "Иванов Иван",
"username": "ivanov",
"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. Чтобы очистить поле — пустую строку.
Необратимо. В журнале действий остаётся запись о том, что лида удалил ключ, и какой именно.
{ "ok": true }
Задачи
Параметры: status, limit, offset. Формат страницы такой же, как у лидов.
{ "title": "Перезвонить Иванову", "priority": "high", "assignee_ids": [5] }
{ "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-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="))