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>
    


Поведение при очереди

Если накопилось много задач:

  1. Сначала объединить одинаковые чтения. Например, не делать 20 запросов subject/index, если можно один раз обновить кеш.
  2. Дедуплицировать запись по внешнему ID.
  3. Запускать чтение страниц последовательно.
  4. Для массового импорта ограничить размер 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/событии от вашей админки, если оператор поменял настройки интеграции.

Как обновлять справочники

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


Рекомендуемый порядок первичной синхронизации

  1. Авторизоваться и сохранить токен.
  2. Получить активные филиалы: branch/index.
  3. Для каждого нужного филиала последовательно прогреть справочники:
    • subject/index;
    • lesson-type/index;
    • location/index;
    • room/index;
    • lead-source/index;
    • lead-status/index;
    • study-status/index;
    • pipeline/index;
    • tariff/index;
    • финансовые справочники, если интеграция работает с оплатами.
  4. Синхронизировать основные сущности:
    • customer/index для клиентов и лидов;
    • group/index;
    • teacher/index;
    • lesson/index по периодам;
    • customer-tariff/index по конкретным клиентам, если нужны абонементы.
  5. Сохранить локальные id ALFACRM и внешние ID интегратора в таблице соответствий.
  6. Запустить инкрементальные задания.

Инкрементальная синхронизация

Для сущностей с полями 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
email 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 и превышение времени ответа.


Чеклист готовности интегратора

Empty

По вашему запросу ничего не найдено

Перейти

Навигация

Esc

Закрыть