v2api: практическая схема интеграции
Документ описывает рекомендуемый клиентский контур для интеграторов ALFACRM v2 API: авторизация, хранение токена, ограничение 5 RPS, кеширование справочников, пагинация, повторы и базовая синхронизация.
Коротко
- Все CRUD-запросы v2 API выполняются по HTTPS и JSON.
- Авторизация:
POST /v2api/auth/loginсemailиapi_key. - Рабочий токен передается в заголовке
X-ALFACRM-TOKEN. - Токен живет 3600 секунд без активности; успешные авторизованные запросы продлевают срок активности на сервере.
- Лимит для интегратора - не более 5 запросов в секунду на любые методы, включая авторизацию.
- Списочные методы используют
pageс нуля:page=0- первая страница. pageSizeподдерживается реализацией, по умолчанию 50, максимум 500.- Большинство списочных методов v2 вызываются через
POST .../indexс фильтрами в JSON body.
Базовая схема интегратора
Ключевая идея: вся интеграция должна ходить в ALFACRM через один общий API-клиент или через общий rate limiter. Нельзя давать каждому воркеру собственные 5 RPS, иначе параллельные процессы легко превысят общий лимит.
Базовые URL и филиал
Авторизация:
<code>POST https://{hostname}/v2api/auth/login
</code>
Филиальные методы:
<code>POST https://{hostname}/v2api/{branch}/{controller}/{action}
</code>
Примеры:
<code>POST /v2api/2/customer/index
POST /v2api/2/lesson/index
POST /v2api/2/subject/index
POST /v2api/2/customer/update?id=123
</code>
Где:
| Параметр | Значение |
|---|---|
{hostname}
|
домен клиента, например demo.s20.online
|
{branch}
|
ID активного филиала |
{controller}
|
сущность API, например customer, lesson, subject
|
{action}
|
действие, обычно index, create, update, delete
|
Для методов, завязанных на филиал, лучше всегда указывать филиал в path. Публичная документация отдельно предупреждает, что если филиал 1 отключен, нужно обращаться через активный ID филиала, например /v2api/2/branch/index.
Заголовки
Для авторизации:
<code><span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Accept</span><span class="hljs-punctuation">: </span>application/json
</code>
Для рабочих запросов:
<code><span class="hljs-attribute">Content-Type</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">Accept</span><span class="hljs-punctuation">: </span>application/json
<span class="hljs-attribute">X-ALFACRM-TOKEN</span><span class="hljs-punctuation">: </span>{token}
</code>
В клиентском контуре используйте только заголовок X-ALFACRM-TOKEN. Не храните и не передавайте токен в URL: URL попадает в access logs, историю браузера, APM и внешние прокси.
Авторизация
Получение токена
<code>curl -i -X POST \
-H <span class="hljs-string">'Content-Type: application/json'</span> \
-H <span class="hljs-string">'Accept: application/json'</span> \
-d <span class="hljs-string">'{"email":"{email}","api_key":"{api_key}"}'</span> \
<span class="hljs-string">'https://{hostname}/v2api/auth/login'</span>
</code>
Успешный ответ:
<code><span class="hljs-punctuation">{</span>
<span class="hljs-attr">"token"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"GENERATED-TOKEN"</span>
<span class="hljs-punctuation">}</span>
</code>
Сервер также возвращает этот же токен в заголовке X-ALFACRM-TOKEN.
Требования к пользователю
Создайте отдельного пользователя CRM только для API-интеграции:
- отдельный email;
- отдельный API key из профиля пользователя;
- минимально достаточные роли и ACL-доступ к модулю
v2api; - доступ только к нужным филиалам;
- запрет использования личного аккаунта сотрудника.
Такой доступ проще отключить, переиздать или ограничить без влияния на работу сотрудников.
Как хранить токен
Храните два типа секретов отдельно:
| Что | Как хранить | Зачем |
|---|---|---|
email и api_key
|
secrets manager, env vault, KMS, зашифрованная конфигурация | это долгоживущие учетные данные |
временный token
|
encrypted cache / DB / Redis с TTL | это рабочий токен на 3600 секунд без активности |
Рекомендуемая запись в хранилище токенов:
<code>key: alfacrm:v2:token:{hostname}:{email_hash}
value:
token: encrypted string
expires_at: received_at + 3300 seconds
refreshed_at: timestamp
refresh_lock_until: timestamp|null
</code>
Почему 3300, а не 3600: это технический запас на сетевые задержки, рассинхронизацию времени и очередь запросов.
Алгоритм работы с токеном
Правила:
- Не логиниться перед каждым запросом. Это тратит лимит и повышает риск блокировки.
- При
401выполнить ровно одно обновление токена и один повтор исходного запроса. - Если повтор после обновления снова вернул
401, остановить обработку и поднять ошибку конфигурации: неверныйapi_key, выключен пользователь, нет прав или истек доступ пользователя. - Делать single-flight refresh: если 10 воркеров одновременно увидели истекший токен, логин делает только один.
Ограничение 5 RPS
Публичная документация задает общий лимит: максимум 5 обращений к API в секунду для любых методов. Практически это означает:
- авторизация тоже считается запросом;
- чтение справочников тоже считается запросом;
- параллельные воркеры должны делить один лимитер;
- лучше держать рабочую скорость 4 RPS, а 5 RPS использовать как жесткий потолок.
Рекомендуемый limiter
Используйте token bucket или leaky bucket.
<code>capacity: 5 tokens
refill: 5 tokens / second
minimum spacing: 200 ms between requests
recommended spacing: 220-250 ms
scope: hostname + API user, optionally hostname only
</code>
Для распределенной интеграции limiter должен жить в Redis, Postgres advisory lock или другом общем хранилище.
Псевдокод:
<code>before every HTTP request:
bucket = load(hostname)
refill tokens by elapsed time
if tokens >= 1:
tokens -= 1
save bucket
send request
else:
sleep until next token
retry acquire
</code>
Поведение при очереди
Если накопилось много задач:
- Сначала объединить одинаковые чтения. Например, не делать 20 запросов
subject/index, если можно один раз обновить кеш. - Дедуплицировать запись по внешнему ID.
- Запускать чтение страниц последовательно.
- Для массового импорта ограничить размер batch так, чтобы он успевал пройти в допустимое окно.
Повторы и backoff
| Код | Что делать |
|---|---|
200, 201
|
обработать ответ |
400
|
не повторять автоматически, исправить данные |
401
|
обновить токен и повторить исходный запрос один раз |
403
|
не повторять, проверить ACL, лицензию, филиал |
404
|
не повторять, проверить ID и филиал |
422
|
не повторять без изменения данных |
429
|
ждать backoff и повторять через limiter |
5xx, network timeout
|
повторить с exponential backoff и jitter, для write-запросов сначала проверить, не была ли операция применена |
Для backoff:
<code>attempt 1: 1s + jitter
attempt 2: 2s + jitter
attempt 3: 4s + jitter
attempt 4: 8s + jitter
max delay: 30s
</code>
Для create и других write-запросов не делайте бесконечных повторов. В v2 API нет универсального idempotency key, поэтому сетевой timeout после отправки может означать, что запись уже создана. Надежнее хранить внешний ID в локальной БД или пользовательском поле CRM и перед повтором искать запись.
Форматы ответов
Список
<code><span class="hljs-punctuation">{</span>
<span class="hljs-attr">"total"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">150</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"count"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">50</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"page"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"items"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
<span class="hljs-punctuation">{</span> <span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span> <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code>
Где:
| Поле | Значение |
|---|---|
total
|
всего записей по фильтру |
count
|
сколько записей пришло в текущей странице |
page
|
текущая страница, с нуля |
items
|
массив моделей |
Create / update
<code><span class="hljs-punctuation">{</span>
<span class="hljs-attr">"success"</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"errors"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"model"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">123</span>
<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code>
При ошибке валидации:
<code><span class="hljs-punctuation">{</span>
<span class="hljs-attr">"success"</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"errors"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"name"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">"Поле обязательно для заполнения"</span><span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"model"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">null</span></span>
<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code>
Пагинация
Важные правила
- Первая страница:
page=0. - Следующая страница:
page=1. - Если
pageне передан, сервер вернет первую страницу. pageSizeпо умолчанию 50.- Максимальный
pageSizeв текущей реализации - 500. pageиpageSizeможно передавать в JSON body; это предпочтительный вариант для v2.
Пример:
<code>curl -s -X POST \
-H <span class="hljs-string">'Content-Type: application/json'</span> \
-H <span class="hljs-string">'Accept: application/json'</span> \
-H <span class="hljs-string">'X-ALFACRM-TOKEN: {token}'</span> \
-d <span class="hljs-string">'{"page":0,"pageSize":500,"is_study":1}'</span> \
<span class="hljs-string">'https://{hostname}/v2api/{branch}/customer/index'</span>
</code>
Правильный цикл чтения страниц
<code>page = 0
pageSize = 500
received = 0
loop:
response = POST index { filters..., page, pageSize }
process response.items
received += response.count
if response.count == 0:
stop
if received >= response.total:
stop
if response.count < pageSize:
stop
page += 1
</code>
Что может пойти не так
Offset-пагинация чувствительна к изменениям данных во время выгрузки. Если во время полного чтения кто-то создает, архивирует или изменяет записи, страницы могут сдвигаться.
Для критичных выгрузок:
- фиксируйте фильтр по периоду, например
date_from/date_to; - для сущностей с
updated_atиспользуйте инкрементальную синхронизацию по времени изменения; - большие периоды режьте на окна по дням или неделям;
- после полной выгрузки делайте короткий догоняющий проход по
updated_at_from, равному времени старта выгрузки минус небольшой overlap.
Кеширование справочников
Справочники нужны для преобразования ID в понятные значения и для подготовки write-запросов. Не нужно запрашивать их перед каждой операцией.
Ключ кеша
<code>alfacrm:v2:dict:{hostname}:{branch}:{controller}:{hash(filters)}
</code>
В ключ обязательно включать:
- hostname клиента;
- ID филиала;
- endpoint;
- фильтры (
active,is_active,pageSizeи т.д.); - версию API, если один сервис работает с несколькими версиями.
Рекомендуемые TTL
| Данные | Endpoint | TTL | Комментарий |
|---|---|---|---|
| филиалы | branch/index
|
15-60 минут | обновлять чаще, если интеграция сама создает или отключает филиалы |
| предметы | subject/index
|
15-60 минут | чаще использовать {"active":1}
|
| типы уроков | lesson-type/index
|
15-60 минут | нужны для занятий и расписания |
| локации и аудитории | location/index, room/index
|
15-60 минут | завязаны на филиал |
| статусы лидов и учеников | lead-status/index, study-status/index
|
30-120 минут | меняются редко |
| причины отказа | lead-reject/index, customer-reject/index
|
30-120 минут | меняются редко |
| источники лидов | lead-source/index
|
30-120 минут | меняются редко, но важны для импорта лидов |
| воронки | pipeline/index
|
30-120 минут | кешировать по филиалу |
| тарифы | tariff/index
|
5-30 минут | влияют на продажи и абонементы |
| счета, статьи, категории оплат | pay-account/index, pay-item/index, pay-item-category/index, pay-type/index
|
5-30 минут | лучше инвалидировать после финансовых настроек |
| скидки | discount/index
|
5-30 минут | зависят от периодов и тарифов |
| преподаватели и пользователи | teacher/index, user/index
|
2-10 минут | это уже почти операционные данные, а не чистый справочник |
Для справочников не нужно проходить все страницы при каждом запросе. Обновляйте кеш фоново и отдавайте бизнес-операциям локальную копию.
Когда инвалидировать кеш
Принудительно сбрасывайте справочник:
- после успешного
createилиupdateэтой же сущности через вашу интеграцию; - если write-запрос вернул ошибку "ID не найден", "недоступно в филиале" или похожую ошибку связанной сущности;
- при смене филиала;
- при смене API-пользователя;
- при ручном webhook/событии от вашей админки, если оператор поменял настройки интеграции.
Как обновлять справочники
Важно заменять кеш атомарно: пока новый справочник не загружен полностью, рабочие процессы должны видеть старую консистентную версию.
Рекомендуемый порядок первичной синхронизации
- Авторизоваться и сохранить токен.
- Получить активные филиалы:
branch/index. - Для каждого нужного филиала последовательно прогреть справочники:
subject/index;lesson-type/index;location/index;room/index;lead-source/index;lead-status/index;study-status/index;pipeline/index;tariff/index;- финансовые справочники, если интеграция работает с оплатами.
- Синхронизировать основные сущности:
customer/indexдля клиентов и лидов;group/index;teacher/index;lesson/indexпо периодам;customer-tariff/indexпо конкретным клиентам, если нужны абонементы.
- Сохранить локальные
idALFACRM и внешние ID интегратора в таблице соответствий. - Запустить инкрементальные задания.
Инкрементальная синхронизация
Для сущностей с полями updated_at используйте водяной знак:
<code>last_successful_sync_at
overlap = 5 minutes
updated_at_from = last_successful_sync_at - overlap
</code>
Почему нужен overlap: разные системы могут округлять время и выполнять фоновые обновления с задержкой. Дубликаты при повторном чтении должны схлопываться по id ALFACRM.
Для занятий лучше читать по окнам дат:
<code>lesson/index:
date_from = YYYY-MM-DD
date_to = YYYY-MM-DD
page = 0
pageSize = 500
</code>
Публичная документация указывает, что если в lesson/index не передать status, по умолчанию используется статус 3 - проведенное занятие. Поэтому для календаря будущих занятий явно передавайте status=1, для отмененных - status=2, для проведенных - status=3.
Работа с основными сущностями
Клиенты и лиды
Endpoint:
<code>POST /v2api/{branch}/customer/index
POST /v2api/{branch}/customer/create
POST /v2api/{branch}/customer/update?id={id}
</code>
Рекомендации:
- Всегда хранить локальное соответствие
external_id -> customer.id. - Для лидов и учеников явно передавать нужные статусы и источники из кеша справочников.
- Не перезаписывать контактные данные без необходимости. В модели есть
refresh_contacts: если передать его какtrue, текущие контакты будут заменены. - Для пользовательских полей использовать имена полей из CRM, обычно
custom_*. - Для поиска по интеграционному ID лучше выделить отдельное пользовательское поле.
Занятия
Endpoint:
<code>POST /v2api/{branch}/lesson/index
POST /v2api/{branch}/lesson/create
POST /v2api/{branch}/lesson/update?id={id}
POST /v2api/{branch}/lesson/teach?id={id}
</code>
Рекомендации:
- Читать занятия только по ограниченному периоду.
- Всегда передавать
status, если логика зависит от статуса. - Для создания использовать ID предметов, типов уроков, аудиторий, локаций и преподавателей из кеша.
- Для отметки проведенного занятия через
teachпередаватьbranch_idтекущего филиала и непустой массивdetails.
Абонементы клиента
Endpoint:
<code>POST /v2api/{branch}/customer-tariff/index?customer_id={customer_id}
POST /v2api/{branch}/customer-tariff/create?customer_id={customer_id}
POST /v2api/{branch}/customer-tariff/update?id={id}&customer_id={customer_id}
POST /v2api/{branch}/customer-tariff/delete?id={id}&customer_id={customer_id}
POST /v2api/{branch}/customer-tariff/recalculate?id={id}&customer_id={customer_id}
</code>
Рекомендации:
- Не выгружать все абонементы глобально, если нужен только конкретный клиент.
- После изменения абонемента перечитывать карточку клиента или абонемент, потому что баланс и счетчики могут пересчитываться.
- Для массовых изменений закладывать очередь и лимитер, так как пересчет может быть тяжелым.
Финансы
Endpoint:
<code>POST /v2api/{branch}/pay/index
POST /v2api/{branch}/pay/create
POST /v2api/{branch}/pay/update?id={id}
</code>
Рекомендации:
- Перед созданием платежа проверить справочники
pay-type,pay-account,pay-item,pay-item-category. - После создания платежа перечитать платеж по
idили клиента, если локальной системе нужен актуальный баланс. - Не ретраить создание платежа без проверки результата: финансовые операции должны быть идемпотентны на стороне интегратора.
Таблица часто используемых endpoint
| Назначение | Endpoint |
|---|---|
| авторизация | POST /v2api/auth/login
|
| филиалы | POST /v2api/{branch}/branch/index или POST /v2api/branch/index
|
| клиенты и лиды | POST /v2api/{branch}/customer/index
|
| группы | POST /v2api/{branch}/group/index
|
| участие клиента в группе | POST /v2api/{branch}/cgi/index?group_id={id}, POST /v2api/{branch}/cgi/customer?customer_id={id}
|
| занятия | POST /v2api/{branch}/lesson/index
|
| регулярное расписание | POST /v2api/{branch}/regular-lesson/index
|
| преподаватели | POST /v2api/{branch}/teacher/index
|
| ставки преподавателей | POST /v2api/{branch}/teacher/teacher-rate
|
| график преподавателей | POST /v2api/{branch}/teacher/working-hour
|
| пользователи | POST /v2api/{branch}/user/index
|
| предметы | POST /v2api/{branch}/subject/index
|
| типы уроков | POST /v2api/{branch}/lesson-type/index
|
| локации | POST /v2api/{branch}/location/index
|
| аудитории | POST /v2api/{branch}/room/index
|
| источники лидов | POST /v2api/{branch}/lead-source/index
|
| статусы лидов | POST /v2api/{branch}/lead-status/index
|
| статусы учеников | POST /v2api/{branch}/study-status/index
|
| причины отказа лидов | POST /v2api/{branch}/lead-reject/index
|
| причины отказа клиентов | POST /v2api/{branch}/customer-reject/index
|
| воронки | POST /v2api/{branch}/pipeline/index
|
| тарифы | POST /v2api/{branch}/tariff/index
|
| абонементы клиента | POST /v2api/{branch}/customer-tariff/index?customer_id={id}
|
| платежи | POST /v2api/{branch}/pay/index
|
| кассовые счета | POST /v2api/{branch}/pay-account/index
|
| типы оплат | POST /v2api/{branch}/pay-type/index
|
| статьи оплат | POST /v2api/{branch}/pay-item/index
|
| категории статей оплат | POST /v2api/{branch}/pay-item-category/index
|
| скидки | POST /v2api/{branch}/discount/index
|
| задачи | POST /v2api/{branch}/task/index
|
| коммуникации | POST /v2api/{branch}/communication/index
|
| SMS | POST /v2api/{branch}/sms-message/index или POST /v2api/{branch}/sms-message
|
POST /v2api/{branch}/mail-message/index или POST /v2api/{branch}/mail-message
|
|
| звонки | POST /v2api/{branch}/phone-call/index или POST /v2api/{branch}/phone-call
|
| журнал изменений | POST /v2api/{branch}/log/index
|
Минимальный контракт API-клиента
Интеграционный клиент должен иметь единый метод запроса:
<code>request(endpoint, body, options):
waitRateLimit()
token = tokenProvider.get()
response = http.post(endpoint, body, X-ALFACRM-TOKEN=token)
if response.status == 401:
tokenProvider.invalidate()
token = tokenProvider.refreshSingleFlight()
waitRateLimit()
response = http.post(endpoint, body, X-ALFACRM-TOKEN=token)
if response.status in retryable:
retryWithBackoffThroughRateLimiter()
if response.status >= 400:
throw ApiError(status, response.body)
return response.body
</code>
И отдельный метод чтения всех страниц:
<code>listAll(controller, filters):
page = 0
pageSize = 500
all = []
while true:
result = request("/{controller}/index", filters + {page, pageSize})
all += result.items
if result.count == 0 or result.count < pageSize or len(all) >= result.total:
return all
page += 1
</code>
Наблюдаемость и аудит
Логируйте:
- hostname;
- branch;
- endpoint;
- HTTP status;
- длительность запроса;
- номер попытки;
- размер ответа;
total,count,pageдля списков;successиerrorsдля write-запросов;- correlation ID вашей системы.
Не логируйте:
api_key;X-ALFACRM-TOKEN;- полные персональные данные клиентов без необходимости;
- HTML платежных виджетов;
- полные тела запросов с телефонами, email и паспортными данными.
Безопасность
- Работать только по HTTPS.
- Не вызывать v2 API напрямую из браузера конечного пользователя: токен и API key нельзя отдавать во frontend.
- Ставить backend-proxy интегратора между frontend и ALFACRM.
- Хранить
api_keyотдельно от временного токена. - Шифровать токены at rest.
- Ротировать API key при смене подрядчика или компрометации.
- Ограничивать пользователя API ролями, филиалами и ACL.
- Завести алерты на рост
401,403,5xxи превышение времени ответа.