- Главная
- Документация
- Документация API
Документация API
Ключи компании, заявки, звонки, записи разговоров, ошибки и примеры.
Версия API: v1. Базовый адрес: https://crm.linkodium.com.
API нужен, чтобы заявки и звонки попадали в CRM автоматически:
- заявки с форм сайта, из квизов, чат-ботов и рекламных кабинетов;
- звонки из любой АТС (Novofon, Zadarma, Mango, Asterisk и других) вместе с записями разговоров.
Содержание
- Ключ API
- Аутентификация
- Формат запросов и ответов
- Ошибки
- Лимиты
- Заявки: POST /api/v1/leads
- Телефония: POST /api/v1/calls
- Запись звонка файлом: POST /api/v1/calls/recording
- Пример прослойки для Novofon и Zadarma
- Переход со старых адресов
1. Ключ API
Ключ выпускает администратор компании: Настройки компании -> API-ключи -> Выпустить ключ. При выпуске ключу дают название (например, «Форма на сайте»), по нему ключ потом легко найти в списке.
- Ключ выглядит так:
crm_и 40 символов0-9a-f, напримерcrm_3f9a.... - Полное значение показывается один раз, сразу после выпуска. Скопируйте его и сохраните в настройках своего сервера. CRM хранит только отпечаток ключа и показать его снова не может. В списке ключей видны только первые символы. Если ключ потерян, выпустите новый и отзовите старый.
- Ключ привязан к компании: всё, что приходит с ним, попадает в эту компанию. Выбрать другую компанию полем запроса нельзя.
- Заведите отдельный ключ на каждую интеграцию (форма сайта, телефония, чат-бот). Тогда при утечке одного ключа достаточно отозвать только его.
- Отозвать ключ можно там же, в списке ключей. Отзыв действует сразу.
- Кнопка «Выйти на всех устройствах» в профиле ключи не отзывает: ключ принадлежит компании, а не сотруднику. Исключение сотрудника из компании, смена его роли и его выход из компании тоже не отзывают выпущенные им ключи сами: для этого есть отметка «Отозвать API-ключи» в карточке сотрудника и у кнопки «Покинуть компанию».
- У компании может быть до 20 действующих ключей.
Где ключ хранить нельзя: в коде страницы сайта, в JavaScript, в мобильном приложении, в публичном репозитории, в адресе запроса (?key=...). Адрес запроса попадает в журналы серверов и историю браузера, поэтому ключ из адреса CRM не читает вовсе.
2. Аутентификация
Ключ передаётся в заголовке запроса. Подходит любой из двух вариантов:
X-API-Key: crm_0123456789abcdef0123456789abcdef01234567
Authorization: Bearer crm_0123456789abcdef0123456789abcdef01234567
Нет ключа, ключ неверный или отозван, компания заблокирована: ответ 401 UNAUTHORIZED.
3. Формат запросов и ответов
- Только HTTPS и только метод
POST. - Тело запроса: JSON-объект (
Content-Type: application/json, кодировка UTF-8) или обычная форма (application/x-www-form-urlencoded,multipart/form-data). Запись звонка файлом: толькоmultipart/form-data. - Тело, которое начинается с
{или[, разбирается как JSON при любомContent-Type. Если это не корректный JSON-объект (ошибка в JSON или массив вместо объекта), ответ400 VALIDATION_ERROR. - Ответ всегда JSON в UTF-8, в том числе отказы, которые сервер выдаёт до разбора запроса: слишком частые запросы (
429), слишком большое тело (413), технические работы (503).
Успешный ответ:
{
"success": true,
"data": { "lead_number": 128, "is_duplicate": false, "message": "Заявка создана" }
}
Ответ с ошибкой:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Ошибка валидации данных",
"details": ["Некорректный формат email"]
}
}
lead_number: номер заявки внутри вашей компании, тот же, что менеджеры видят в CRM (#128).- Неизвестные поля запроса молча игнорируются.
- Ответы не кэшируются (
Cache-Control: no-store). - В каждом ответе есть заголовок
X-Request-ID. Если пишете в поддержку о конкретном запросе, приложите его значение.
4. Ошибки
| HTTP | error.code | Что случилось | Что делать |
|---|---|---|---|
| 400 | VALIDATION_ERROR | Данные не прошли проверку. Список причин в error.details | Исправить запрос. Повторять без изменений бесполезно |
| 401 | UNAUTHORIZED | Нет ключа, ключ неверный или отозван, компания заблокирована | Проверить заголовок и ключ |
| 403 | QUOTA_EXCEEDED | Хранилище компании заполнено, запись звонка файлом не принята (раздел 8) | Освободить место или обратиться в поддержку |
| 404 | CALL_NOT_FOUND | Звонка с таким call_id в CRM нет: пришло событие без номера клиента, а начала звонка ещё не было (раздел 7), или загружается файл к незаведённому звонку (раздел 8) | Если начало звонка может прийти позже, повторить через минуту. Для записи исходящего звонка, который не сохранялся (раздел 7), это штатный ответ |
| 405 | METHOD_NOT_ALLOWED | Метод не POST | Использовать POST |
| 413 | PAYLOAD_TOO_LARGE | Тело запроса или файл записи больше допустимого (раздел 5) | Уменьшить файл (например, mp3 вместо wav) |
| 429 | RATE_LIMITED | Превышен лимит запросов или одновременных загрузок | Повторить позже, см. заголовок Retry-After |
| 500 | INTERNAL_ERROR | Сбой на стороне CRM | Повторить с паузой |
| 503 | MAINTENANCE | Идут технические работы | Повторить через несколько минут, см. Retry-After |
| 503 | SERVICE_UNAVAILABLE | Сервис временно недоступен | Повторить через минуту, см. Retry-After |
Рекомендация по повторам: при 429, 500, 503 и обрыве связи повторяйте запрос с растущей паузой (например, 10 секунд, 1 минута, 5 минут), а если в ответе есть заголовок Retry-After, не раньше указанного в нём числа секунд. Повтор безопасен: /api/v1/calls и загрузка записи идемпотентны, а повторная заявка с тем же телефоном или email распознаётся как повторное обращение (см. раздел 6). 404 CALL_NOT_FOUND повторяйте, только если начало звонка ещё может прийти. Ошибки 400, 401, 405, 413 повтором не исправляются.
Отказ принять запись разговора по ссылке ошибкой всего запроса не считается: звонок и заявка сохраняются, а причина отказа приходит в ответе полем recording_error (раздел 7).
5. Лимиты
| Что | Лимит |
|---|---|
| Запросов на один ключ | 3000 за 5 минут |
| Запросов с одного IP-адреса | мягкое ограничение от флуда, живая интеграция в него не упирается |
| Одновременных загрузок записи файлом с одного IP-адреса | 8 |
| Размер тела запроса (JSON, форма) | 3 МБ |
| Файл записи звонка | 100 МБ |
| Запись звонка по ссылке | до 200 МБ |
| Записей по ссылке, ждущих скачивания | 20 на одну заявку, 200 на компанию, 400 на все компании одного владельца |
| Действующих ключей у компании | 20 |
Превышение лимита запросов или одновременных загрузок: 429 RATE_LIMITED с заголовком Retry-After.
Очередь записей по ссылке. Пока у компании меньше 20 записей ждут скачивания, её новые записи принимаются всегда, как бы ни была загружена очередь остальных компаний (предел в 20 записей на одну заявку действует и тогда). Если очередь заполнена (своя, общая для компаний владельца или общая для сервиса), звонок всё равно сохраняется, а запись получает отказ QUEUE_FULL в поле recording_error (раздел 7). Живая телефония держит в очереди единицы записей и в этот потолок не упирается.
6. Заявки: POST /api/v1/leads
Создаёт заявку в компании ключа. Ответственным становится менеджер по умолчанию компании (Настройки компании -> Сотрудники). Сотрудники получают уведомление в Telegram по своим настройкам.
Поля
Обязательно хотя бы одно из полей name, phone, email.
| Поле | Описание |
|---|---|
name | Имя клиента |
phone | Телефон в любом формате, не меньше 7 цифр. Номер короче: ошибка 400 |
email | Email. Адрес в неверном формате: ошибка 400 |
company | Компания клиента |
website | Сайт клиента. Некорректный адрес не сохраняется, но заявку не отменяет |
message | Текст обращения |
client_needs | Что нужно клиенту (например, название формы или квиза) |
source | Источник: название сайта, рекламной кампании, «Звонок» и т. п. |
service | Услуга, любой текст до 255 символов |
status | Этап заявки: номер (1, 2, 5, 6, 7) или название («Новая заявка», «В работе», «Успешно», «Провал», «Спам»; регистр букв не важен). По умолчанию «Новая заявка». Другое значение: ошибка 400 |
landing_page | Страница первого захода посетителя |
form_page | Страница, с которой отправлена форма |
referrer | Откуда пришёл посетитель |
ip_address | IP-адрес посетителя |
user_agent | Браузер посетителя |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | Метки UTM |
ym_client_id | ClientID Яндекс Метрики (латиница, цифры, ., _, -, до 64 символов; иное значение не сохраняется) |
Заявка не теряется из-за длины: слишком длинные значения обрезаются по ширине поля (phone 50 символов, landing_page, form_page, referrer 500, ip_address 45, ym_client_id 64, message, client_needs и user_agent около 65 000 байт, остальные строки 255 символов). Байты не в UTF-8 заменяются знаком замены (U+FFFD).
ip_address, user_agent, referrer передавайте с данными посетителя сайта. CRM не подставляет вместо них адрес вашего сервера.
Заявка со статусом «Спам» сохраняется, но уведомление о ней не отправляется. Так удобно передавать заявки, которые отметил антиспам формы: если он ошибся, заявку видно в CRM под фильтром «Спам».
Повторное обращение
Если в компании уже есть активная заявка (этап «Новая заявка» или «В работе», не в архиве) с тем же телефоном (сравниваются последние 10 цифр) или тем же email, новая заявка не создаётся. В существующую добавляется событие «Повторное обращение» с полями source, name, company, phone, email, service, message, client_needs, form_page, ip_address, метками UTM (utm_source, utm_medium, utm_campaign, utm_term, utm_content), ym_client_id, landing_page и referrer, ответ 200 с is_duplicate: true и номером существующей заявки. Уведомление при этом не отправляется.
Остальные поля повторного обращения (website, user_agent, status) не сохраняются. Поля самой существующей заявки не меняются: метки и ym_client_id повторного обращения лежат в его событии в ленте заявки, а не в карточке.
Телефон, в котором меньше 10 цифр (например, городской номер без кода города), для поиска повторного обращения не используется: по короткому хвосту он совпал бы с номером другого клиента. Такая заявка сверяется только по email.
Ответы
Новая заявка, 201 Created:
{ "success": true, "data": { "lead_number": 128, "is_duplicate": false, "message": "Заявка создана" } }
Повторное обращение, 200 OK:
{ "success": true, "data": { "lead_number": 97, "is_duplicate": true, "message": "Повторное обращение добавлено к существующей заявке" } }
Пример: curl
curl -X POST https://crm.linkodium.com/api/v1/leads \
-H "X-API-Key: $CRM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Анна",
"phone": "+7 999 123-45-67",
"email": "anna@example.com",
"message": "Нужна консультация",
"source": "Сайт example.com",
"form_page": "https://example.com/contacts/",
"utm_source": "yandex",
"utm_campaign": "brand"
}'
Пример: PHP
<?php
// Ключ храните в настройках сервера, не в коде страницы.
$apiKey = getenv('CRM_API_KEY');
$lead = [
'name' => $_POST['name'] ?? '',
'phone' => $_POST['phone'] ?? '',
'email' => $_POST['email'] ?? '',
'message' => $_POST['message'] ?? '',
'source' => 'Сайт example.com',
'form_page' => $_SERVER['HTTP_REFERER'] ?? '',
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
];
$ch = curl_init('https://crm.linkodium.com/api/v1/leads');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($lead, JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-API-Key: ' . $apiKey],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$resp = json_decode((string)$body, true);
// 201 - новая заявка, 200 - повторное обращение: оба случая означают «заявка у менеджера».
if (in_array($code, [200, 201], true) && !empty($resp['success'])) {
$number = $resp['data']['lead_number'];
} else {
// Заявку не потеряйте: сохраните её у себя или отправьте письмом и повторите позже.
error_log('CRM: HTTP ' . $code . ' ' . $body);
}
7. Телефония: POST /api/v1/calls
Универсальный приём звонков от любой АТС. Прослойку между АТС и CRM пишете вы (или берёте готовую), она переводит события АТС в вызовы этого метода. Пример для Novofon и Zadarma в разделе 9.
Модель
Звонок в CRM определяется парой «компания + call_id», где call_id это идентификатор звонка на стороне АТС. Каждое событие звонка (начало, ответ, конец, готовая запись) присылайте отдельным запросом с одним и тем же call_id:
- Первый запрос с новым
call_idзаводит звонок в ленте заявки. - Следующие запросы с тем же
call_idдописывают поля звонка: итог, длительность, время, запись. Непереданные и пустые поля сохраняют прежние значения. - Повтор одного и того же запроса ничего не портит. Если не получили ответ, смело отправляйте ещё раз.
Хранить у себя соответствие «звонок АТС -> заявка CRM» не нужно: заявку по номеру клиента находит сама CRM.
Как CRM находит или создаёт заявку
При первом событии звонка (в компании ещё нет звонка с этим call_id):
- Нужен
client_phone, номер клиента. Без него ответ404 CALL_NOT_FOUND: звонок не заведён и ничего не сохранено. Так штатно выглядит, например, событие готовой записи исходящего звонка, который не сохранялся (см. ниже), или событие, которое пришло раньше начала звонка. Некорректный номер (меньше 7 цифр) в первом событии:400. - CRM ищет активную заявку компании (этап «Новая заявка» или «В работе», не в архиве) с этим номером (сравниваются последние 10 цифр; номер короче 10 цифр активную заявку не находит). Нашла: звонок попадает в неё.
- Не нашла:
- входящий звонок создаёт новую заявку: телефон клиента, источник
source(по умолчанию «Звонок»), услугаservice, ответственный менеджер по умолчанию. Сотрудники получают уведомление «Новая заявка» по своим настройкам; - исходящий звонок создаёт заявку, только если в компании включена настройка Настройки компании -> «Исходящий звонок на номер без заявки создаёт новую заявку». Если выключена (так по умолчанию), звонок не сохраняется, ответ
200сskipped: true.
- входящий звонок создаёт новую заявку: телефон клиента, источник
Отдельных уведомлений о звонках нет: звонок виден в ленте заявки.
Поля
| Поле | Обязательно | Описание |
|---|---|---|
call_id | да | Идентификатор звонка в АТС: от 1 до 100 символов из латинских букв, цифр и _ . -, хотя бы одна буква или цифра |
client_phone | в первом событии | Номер клиента (не меньше 7 цифр). В последующих событиях можно не передавать; некорректное значение (например, anonymous) в них игнорируется |
direction | нет | in (входящий, по умолчанию) или out (исходящий). Принимаются также incoming, outgoing, inbound, outbound. Другое значение: ошибка 400 |
line | нет | Ваш номер или линия: куда звонил клиент (для входящего) или с какого номера звонили (для исходящего). До 100 символов |
status | нет | Итог звонка, см. словарь ниже. До 100 символов |
duration | нет | Длительность разговора в секундах, целое число от 0 до 604800 |
started_at | нет | Время начала звонка в ISO-8601 со смещением часового пояса, например 2026-09-28T14:05:00+03:00 или 2026-09-28T11:05:00Z. Время без смещения отклоняется с ошибкой 400 |
source | нет | Источник для новой заявки (по умолчанию «Звонок») |
service | нет | Услуга для новой заявки, любой текст |
record_url | нет | Ссылка на запись разговора, см. ниже |
Словарь status (регистр и разделители не важны: no answer, no_answer, NO-ANSWER одинаковы):
| Значение | В карточке |
|---|---|
answered | Принят |
no answer | Нет ответа |
missed | Пропущен |
busy | Занято |
cancel, canceled, cancelled | Отменён |
failed | Ошибка |
congestion | Перегрузка |
unallocated number | Номер не существует |
no money | Недостаточно средств |
unallowed destination | Направление запрещено |
Другое значение показывается в карточке как есть.
Время и часовой пояс
CRM хранит время в UTC и показывает каждому сотруднику в его часовом поясе. Поэтому started_at обязан содержать смещение: 2026-09-28 14:05:00 без смещения можно прочитать по-разному, и в карточке оказалось бы неверное время. Если АТС присылает время без пояса, прослойка должна дописать пояс, в котором работает АТС (обычно он задан в настройках кабинета АТС). Пример в разделе 9.
Запись разговора ссылкой
Если АТС отдаёт запись по ссылке, передайте её в record_url (в том же событии или отдельным запросом с тем же call_id). CRM поставит запись в очередь, скачает её в течение нескольких минут и прикрепит к звонку.
Требования к ссылке:
- схема
httpилиhttps, порт 80 или 443, длина до 2000 символов; - доступна из интернета без авторизации (подписанная ссылка АТС подходит). Логин и пароль в самой ссылке (
https://user:pass@...) не принимаются; - ведёт в интернет: IP-адрес в ссылке допустим только публичный (IPv6 только из глобального диапазона
2000::/3), имена внутренней сети (localhost, имя без точки, домены.local,.internalи подобные) не принимаются. Ссылки во внутреннюю сеть CRM не скачивает; - живёт не меньше нескольких часов: при временной ошибке CRM повторяет скачивание с растущими паузами, на все попытки уходит от получаса и дольше. Если АТС позволяет задать срок жизни ссылки, задайте сутки;
- сервер записи присылает начало ответа (заголовки) не позже чем через 60 секунд, в том числе на каждом редиректе, и отдаёт файл целиком не дольше чем за 10 минут. Если в ответе есть заголовок
Content-Length, CRM через 30 секунд после начала передачи прикидывает, успеет ли файл докачаться за эти 10 минут, и прерывает попытку, если не успеет; безContent-Lengthсредняя скорость отдачи должна быть не ниже 8 КБ/с. Запись, которая не укладывается в эти пределы ни в одной из 5 попыток, к звонку не прикрепляется; - формат mp3, wav, ogg, webm, m4a или aac, размер до 200 МБ.
Повтор той же ссылки для того же звонка не ставит вторую загрузку (ответ recording: "duplicate").
Если запись по ссылке принять нельзя, звонок и заявка всё равно сохраняются: ответ обычный (201 или 200), а в нём recording: "rejected" и поле recording_error с причиной:
recording_error.code | Причина | Что делать |
|---|---|---|
VALIDATION_ERROR | Ссылка не подходит под требования выше, подробности в recording_error.details | Исправить ссылку и прислать её тем же call_id |
QUOTA_EXCEEDED | Хранилище компании заполнено | Освободить место, затем прислать ссылку снова |
QUEUE_FULL | Очередь записей переполнена (раздел 5) | Прислать ссылку тем же call_id через несколько минут |
Проверяйте поле recording в ответе: если запись важна, а пришло rejected, сохраните ссылку у себя и повторите позже или загрузите файл методом из раздела 8.
Если ссылки нет, а файл есть, загрузите его методом из раздела 8.
Ответы
Звонок заведён этим запросом, 201 Created:
{ "success": true, "data": { "lead_number": 128, "call_id": "1727517900.1234", "created_lead": true } }
Звонок уже был, поля дописаны, 200 OK:
{ "success": true, "data": { "lead_number": 128, "call_id": "1727517900.1234", "created_lead": false, "recording": "queued" } }
created_lead:true, если этот запрос создал новую заявку;recording(только если переданrecord_url):queuedпоставлена в очередь,duplicateэта ссылка уже была,rejectedзапись не принята (звонок при этом сохранён);recording_error(только приrecording: "rejected"):code,messageи, для негодной ссылки,details.
Запись не принята, звонок сохранён, 200 OK:
{
"success": true,
"data": {
"lead_number": 128,
"call_id": "1727517900.1234",
"created_lead": false,
"recording": "rejected",
"recording_error": {
"code": "VALIDATION_ERROR",
"message": "Ссылка на запись не принята",
"details": ["record_url: допустим только порт 80 или 443"]
}
}
}
Исходящий звонок на номер без заявки при выключенной настройке, 200 OK:
{ "success": true, "data": { "lead_number": null, "call_id": "out-777", "skipped": true, "reason": "no_lead_for_outgoing" } }
Пример: входящий звонок
Начало звонка:
curl -X POST https://crm.linkodium.com/api/v1/calls \
-H "X-API-Key: $CRM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"call_id": "1727517900.1234",
"direction": "in",
"client_phone": "+79991234567",
"line": "+73422000000",
"started_at": "2026-09-28T14:05:00+05:00",
"source": "Реклама Яндекс"
}'
Конец звонка (тот же call_id):
curl -X POST https://crm.linkodium.com/api/v1/calls \
-H "X-API-Key: $CRM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "call_id": "1727517900.1234", "status": "answered", "duration": 185 }'
Готовая запись:
curl -X POST https://crm.linkodium.com/api/v1/calls \
-H "X-API-Key: $CRM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "call_id": "1727517900.1234", "record_url": "https://records.example-pbx.com/abc123.mp3" }'
Пример: PHP
<?php
function crmCall(array $fields): array
{
$ch = curl_init('https://crm.linkodium.com/api/v1/calls');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($fields, JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-API-Key: ' . getenv('CRM_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
return [curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode((string)$body, true)];
}
[$code, $resp] = crmCall([
'call_id' => '1727517900.1234',
'direction' => 'in',
'client_phone' => '+79991234567',
'started_at' => (new DateTimeImmutable('now'))->format(DATE_ATOM), // со смещением
]);
8. Запись звонка файлом: POST /api/v1/calls/recording
Для АТС, которые не дают публичную ссылку на запись: прослойка скачивает файл сама и отправляет его в CRM.
- Тело: только
multipart/form-dataс полямиcall_idиfile. JSON или форма без файла:400 VALIDATION_ERROR(ссылку на запись передавайте в/api/v1/calls, полеrecord_url). - Звонок с этим
call_idуже должен быть передан в/api/v1/calls, иначе404 CALL_NOT_FOUND. По одному файлу CRM заявку не создаёт: в файле нет номера клиента. - Форматы: mp3, wav, ogg, webm, m4a, aac. CRM проверяет содержимое файла: файл, в котором распознан другой тип (HTML, картинка, архив, документ), отклоняется с ошибкой
400. Если тип по содержимому определить нельзя (так бывает у mp3 без заголовка), решает расширение имени файла из списка выше. Пустой файл:400. - Размер: до 100 МБ, больше:
413 PAYLOAD_TOO_LARGE. - Место в хранилище компании: если его не хватает,
403 QUOTA_EXCEEDED. - Одновременно с одного IP-адреса: до 8 загрузок, сверх этого
429 RATE_LIMITEDсRetry-After. - Повтор того же файла (то же содержимое) для того же звонка второй записи не создаёт: ответ
200сduplicate: trueи данными ранее принятой записи. Имя файла при этом значения не имеет. - У одного звонка может быть несколько записей (например, разговор в двух частях или каналы клиента и оператора отдельными файлами). Разные файлы принимаются все, даже с одинаковыми именем и размером. Имена всё же лучше давать разные (
call-1727517900.1234-1.mp3,...-2.mp3): под этим именем запись видят и скачивают менеджеры. - Если расширение в имени файла не совпадает с форматом, который CRM определила по содержимому, в имени оно заменяется на верное (поле
file_nameответа).
Ответ 201 Created:
{
"success": true,
"data": {
"lead_number": 128,
"call_id": "1727517900.1234",
"recording": { "id": "5d0c7a4e-8f3b-4f5e-9a51-2b7f0c1d9e20", "file_name": "call.mp3", "size": 482113, "type": "audio/mpeg" }
}
}
Повтор уже принятого файла, 200 OK:
{
"success": true,
"data": {
"lead_number": 128,
"call_id": "1727517900.1234",
"duplicate": true,
"recording": { "id": "5d0c7a4e-8f3b-4f5e-9a51-2b7f0c1d9e20", "file_name": "call.mp3", "size": 482113, "type": "audio/mpeg" }
}
}
Пример: curl
curl -X POST https://crm.linkodium.com/api/v1/calls/recording \
-H "X-API-Key: $CRM_API_KEY" \
-F "call_id=1727517900.1234" \
-F "file=@/tmp/call-1727517900.1234.mp3"
Пример: PHP
<?php
$callId = '1727517900.1234';
$part = 1; // номер части, если АТС режет разговор на несколько файлов
$ch = curl_init('https://crm.linkodium.com/api/v1/calls/recording');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'call_id' => $callId,
'file' => new CURLFile('/tmp/call.mp3', 'audio/mpeg', 'call-' . $callId . '-' . $part . '.mp3'),
],
CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('CRM_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// 201 - принята, 200 - этот файл уже был. 429, 5xx и обрыв связи: повторить позже.
if (!in_array($code, [200, 201], true)) {
error_log('CRM recording: HTTP ' . $code . ' ' . $body);
}
9. Пример прослойки для Novofon и Zadarma
Novofon и Zadarma сообщают о звонках вебхуками (уведомлениями о событиях АТС) на адрес, который вы указываете в кабинете АТС. Прослойка это небольшой скрипт на вашем сервере: принимает вебхук, переводит его в вызов /api/v1/calls и сразу отвечает АТС.
Соответствие событий
| Событие АТС | Поля АТС | Вызов CRM |
|---|---|---|
NOTIFY_START (входящий, начало) | pbx_call_id, caller_id, called_did, call_start | call_id = pbx_call_id, direction = in, client_phone = caller_id, line = called_did, started_at = call_start с поясом |
NOTIFY_END (входящий, конец) | pbx_call_id, caller_id, called_did, duration, disposition, call_start | call_id = pbx_call_id, direction = in, client_phone = caller_id, status = disposition, duration = duration |
NOTIFY_OUT_START (исходящий, начало) | pbx_call_id, destination, internal, call_start | call_id = pbx_call_id, direction = out, client_phone = destination, started_at = call_start с поясом |
NOTIFY_OUT_END (исходящий, конец) | pbx_call_id, destination, caller_id, duration, disposition | call_id = pbx_call_id, direction = out, client_phone = destination, line = caller_id, status = disposition, duration = duration |
NOTIFY_RECORD (запись готова) | pbx_call_id, call_id_with_rec | ссылку получить методом API АТС pbx/record/request, затем call_id = pbx_call_id, record_url = ссылка |
Важно:
- В
call_idпередавайтеpbx_call_id: он одинаков у всех событий одного звонка.call_id_with_recнужен только для запроса ссылки на запись. client_phoneпередавайте в каждом событии, где номер есть: если первое событие потеряется, звонок заведёт следующее.call_startу этих АТС приходит без часового пояса, во времени, заданном в кабинете АТС. Прослойка должна дописать этот пояс (в примере константаPBX_TIMEZONE).- Ссылку на запись АТС готовит не сразу; Novofon советует запрашивать её примерно через 40 секунд после уведомления.
- Ответ
404 CALL_NOT_FOUNDнаNOTIFY_RECORDштатен для исходящего звонка, если в компании выключена настройка «Исходящий звонок на номер без заявки создаёт новую заявку»: такой звонок не сохранялся, и запись к нему прикладывать некуда. - Если в ответе
recording: "rejected", запись не принята, хотя звонок сохранён. Причину смотрите вrecording_error(раздел 7).
Пример прослойки (PHP)
<?php
// Прослойка АТС -> Linkodium CRM. Адрес этого скрипта укажите в кабинете АТС
// как адрес уведомлений о звонках.
const CRM_URL = 'https://crm.linkodium.com/api/v1/calls';
const PBX_TIMEZONE = 'Europe/Moscow'; // пояс из настроек кабинета АТС
// Проверка адреса при подключении уведомлений (Novofon, Zadarma).
if (isset($_GET['zd_echo'])) {
exit(preg_replace('/[^A-Za-z0-9]/', '', (string)$_GET['zd_echo']));
}
// Здесь же стоит проверить подпись уведомления (заголовок Signature)
// по документации АТС: иначе прислать «звонок» сможет кто угодно.
// $okCodes - какие ответы кроме 200/201 для этого события норма (не ошибка).
function crmCall(array $fields, array $okCodes = []): void
{
$ch = curl_init(CRM_URL);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(array_filter($fields, fn($v) => $v !== '' && $v !== null),
JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-API-Key: ' . getenv('CRM_API_KEY')],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$resp = json_decode((string)$body, true);
if (!in_array($code, array_merge([200, 201], $okCodes), true)) {
// Ответ и тело сохраните в журнал: по ним видно, что не так с запросом.
// 429, 5xx и обрыв связи стоит повторить позже (раздел 4).
error_log('CRM calls: HTTP ' . $code . ' ' . $body);
} elseif (($resp['data']['recording'] ?? '') === 'rejected') {
// Звонок сохранён, запись нет: причина в recording_error (раздел 7).
error_log('CRM calls: запись не принята ' . json_encode($resp['data']['recording_error'] ?? []));
}
}
// Время АТС «2026-09-28 14:05:00» -> «2026-09-28T14:05:00+03:00».
function pbxTime(string $raw): string
{
$dt = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $raw, new DateTimeZone(PBX_TIMEZONE));
return $dt ? $dt->format(DATE_ATOM) : '';
}
$p = $_POST;
switch ($p['event'] ?? '') {
case 'NOTIFY_START':
crmCall([
'call_id' => $p['pbx_call_id'] ?? '',
'direction' => 'in',
'client_phone' => $p['caller_id'] ?? '',
'line' => $p['called_did'] ?? '',
'started_at' => pbxTime($p['call_start'] ?? ''),
// Источник по номеру линии: каждой рекламной линии свой.
'source' => ['73422000000' => 'Сайт', '79670000000' => 'Реклама'][$p['called_did'] ?? ''] ?? 'Звонок',
]);
break;
case 'NOTIFY_END':
crmCall([
'call_id' => $p['pbx_call_id'] ?? '',
'direction' => 'in',
'client_phone' => $p['caller_id'] ?? '',
'line' => $p['called_did'] ?? '',
'status' => $p['disposition'] ?? '',
'duration' => (int)($p['duration'] ?? 0),
'started_at' => pbxTime($p['call_start'] ?? ''),
]);
break;
case 'NOTIFY_OUT_START':
crmCall([
'call_id' => $p['pbx_call_id'] ?? '',
'direction' => 'out',
'client_phone' => $p['destination'] ?? '',
'started_at' => pbxTime($p['call_start'] ?? ''),
]);
break;
case 'NOTIFY_OUT_END':
crmCall([
'call_id' => $p['pbx_call_id'] ?? '',
'direction' => 'out',
'client_phone' => $p['destination'] ?? '',
'line' => $p['caller_id'] ?? '',
'status' => $p['disposition'] ?? '',
'duration' => (int)($p['duration'] ?? 0),
'started_at' => pbxTime($p['call_start'] ?? ''),
]);
break;
case 'NOTIFY_RECORD':
// Ссылку на запись выдаёт API АТС (метод pbx/record/request, параметр
// call_id = call_id_with_rec, срок жизни ссылки lifetime). Используйте
// официальную библиотеку АТС; pbxRecordLink() здесь условная функция.
$url = pbxRecordLink($p['call_id_with_rec'] ?? '', $p['pbx_call_id'] ?? '');
if ($url) {
// 404: звонок не сохранялся (исходящий при выключенной настройке).
crmCall(['call_id' => $p['pbx_call_id'] ?? '', 'record_url' => $url], [404]);
}
break;
}
header('Content-Type: application/json');
echo json_encode(['success' => true]);
Если ссылку на запись получить нельзя, но файл скачать можно, отправьте его методом из раздела 8 с тем же call_id.
10. Переход со старых адресов
Интеграции Linkodium, подключённые до выхода API v1, пользуются старыми адресами без ключа: /api/lead.add (и /api/leads), /api/lead.call, /api/call.record. Они работают, пока идёт перевод интеграций на v1, и будут отключены. Новые интеграции подключайте только к v1.
| Было | Стало |
|---|---|
POST /api/lead.add без ключа, в ответе lead_id | POST /api/v1/leads с ключом, в ответе lead_number |
POST /api/lead.call с lead_id, phone, called_did, disposition, call_start без пояса | POST /api/v1/calls с call_id, client_phone, line, status, started_at с поясом. lead_id не нужен: заявку находит CRM |
POST /api/call.record с lead_id, record_url | record_url в POST /api/v1/calls с тем же call_id |
| Прослойка хранит у себя соответствие «звонок -> заявка» | Не нужно |
До отключения старые адреса принимают и ключ: с заголовком X-API-Key вызов попадает в компанию ключа. Ключ, который не прошёл проверку, даёт 401 UNAUTHORIZED: в режим без ключа такой запрос не переходит.
Отличия старого /api/call.record от записи ссылкой в v1: этот адрес принимает только запись, поэтому отказ отвечает на весь запрос. Негодная ссылка (требования те же, что в разделе 7): 400 VALIDATION_ERROR с причиной в error.details; хранилище заполнено: 403 QUOTA_EXCEEDED; очередь переполнена: 429 QUEUE_FULL с заголовком Retry-After.