CRM API: синхронизация

← CRM API: начало работы

Cursor-пагинация

Обычные списки используют непрозрачный подписанный cursor:

PLAINTEXT
GET /api/v1/crm/contacts?limit=50
GET /api/v1/crm/contacts?limit=50&cursor=<nextCursor>
  • стандартный limit — 50, максимум — 100;
  • исключение — /search: 20 по умолчанию, максимум 50;
  • следующую позицию берите только из meta.nextCursor;
  • не разбирайте и не изменяйте cursor;
  • сохраняйте сортировку и фильтры между страницами: cursor связан с tenant и набором фильтров.

Подмена cursor, его использование в другой организации или с другим фильтром приводит к 400 invalid_cursor.

externalId и upsert

externalId связывает объект APX Chat с записью внешней системы. Значение уникально в пространстве (tenant, applicationId, resourceType, externalId): разные API-приложения могут использовать свои идентификаторы независимо.

Для contacts, companies, deals и tasks действуют правила:

  • если соответствия ещё нет, upsert создаёт объект без expectedVersion;
  • если соответствие уже есть, требуется его текущий expectedVersion;
  • отсутствие версии при обновлении возвращает 428 precondition_required;
  • несовпадение версии возвращает 409 version_conflict с details.currentVersion;
  • передача expectedVersion при создании нового соответствия возвращает 422 validation_error;
  • повторное использование externalId удалённого объекта возвращает 410 external_reference_deleted.

Храните вместе externalId, UUID APX Chat и последнюю известную version. Не используйте имя, телефон или email как технический ключ синхронизации.

CAS и конкурентные изменения

Изменяемые ресурсы содержат числовую version. Для PATCH, DELETE, move, archive и restore передавайте её как expectedVersion в теле или query — точное место указано в OpenAPI.

Опции дополнительных полей требуют сразу expectedVersion опции и expectedDefinitionVersion определения. Assignments используют отдельную версию набора customFieldsVersion; настройки до первого сохранения используют версию 0.

При 409 version_conflict перечитайте ресурс, объедините изменения и повторите запрос с новой версией. Не подставляйте новую версию автоматически: это может затереть параллельные изменения. Отсутствующее обязательное поле в обычном DTO даёт 400 validation_error; специальный 428 precondition_required используется для upsert существующего externalId.

Идемпотентность mutation-запросов

Каждый POST, PATCH и DELETE из mutation-строк таблиц требует заголовок длиной 1–200 символов:

PLAINTEXT
Idempotency-Key: 9b897d29-f43f-4e9a-a1e1-ef9e74ad3fe3

Ключ хранится 24 часа и изолирован по applicationId. Повтор того же method, path и JSON body возвращает сохранённый статус и тело с Idempotency-Replayed: true. Тот же ключ с другим запросом даёт 409 idempotency_conflict, а параллельный незавершённый запрос — 409 idempotency_in_progress. Отсутствующий или слишком длинный ключ даёт 422 validation_error.

После сетевого таймаута повторяйте ту же бизнес-операцию с тем же ключом. Для новой операции создавайте новый ключ.

Change feed

МетодПутьScopeНазначение
GET/changes/cursorcrm_changes_readТекущая позиция ленты
GET/changescrm_changes_readИзменения после позиции

Надёжный цикл синхронизации:

  1. Перед первоначальной загрузкой получите и сохраните позицию через GET /changes/cursor.
  2. Выполните полную загрузку нужных списков через их cursor-пагинацию.
  3. Читайте GET /changes?after=<сохранённый cursor>&limit=50&resourceTypes=contact,company,deal.
  4. Зафиксируйте страницу у себя и только после этого сохраните непустой meta.nextCursor.
  5. Продолжайте, пока meta.hasMore не станет false; при следующем poll используйте последнюю сохранённую позицию.

Лента возвращает только типы, для которых у приложения есть ресурсный read-scope. События идут по возрастающей sequence, могут быть обработаны повторно после сбоя клиента и должны применяться идемпотентно. Cursor и события хранятся 90 дней. Просроченная позиция возвращает 410 cursor_expired: выполните полную сверку и начните с нового cursor.

CRM webhooks

Подписка создаётся только в кабинете: Настройки → Webhooks. Публичного CRM-маршрута для её управления нет. Выберите событие crm_change, HTTPS URL, HMAC secret и стабильное API-приложение. У приложения должны быть crm_changes_read и read-scope конкретного ресурса. Изменение или отзыв scopes до доставки применяется fail-closed.

У CRM webhook уже в payload v1 используется structured envelope. При выборе payload v2 форма и проекция data сохраняются, но apiVersion становится "v2". Secret обязателен для обеих версий.

Тело события:

JSON
{
  "id": "811793c8-d361-487b-b3f7-1d59b3fe3fc6",
  "type": "crm.deal.updated",
  "apiVersion": "v1",
  "occurredAt": "2026-08-16T10:30:00.000Z",
  "data": {
    "resourceType": "deal",
    "resourceId": "302c748c-04bd-4b15-bf12-e4c9c6767189",
    "externalId": "order-1942",
    "action": "updated",
    "version": 8,
    "changedFields": ["amount", "stageId"]
  }
}

data.action принимает created, updated, deleted, merged, anonymized, completed или failed. resourceId и version могут быть null. externalId присутствует только если это приложение создало соответствующий mapping.

Заголовки доставки:

PLAINTEXT
X-APX-Event-Id: 811793c8-d361-487b-b3f7-1d59b3fe3fc6
X-APX-Event-Timestamp: 1786876200
X-APX-Signature: v1=<hex hmac-sha256>

Подписывается точная строка <timestamp>.<rawBody>. Проверяйте исходные байты до JSON parsing, допустимое отклонение timestamp и подпись сравнением с постоянным временем. Повторные попытки сохраняют тот же event ID; доставка выполняется до 5 раз с экспоненциальной задержкой от 60 секунд. Дедуплицируйте по X-APX-Event-Id, а пропуски восстанавливайте через change feed.