В магазине сто заказов за день, в CRM девяносто девять. Оба сервиса показывают зелёный статус, в журнале вебхуков ошибок нет, интеграция «работает». И всё же один заказ существует только на одной стороне. Начинать поиск стоит именно с этой отсутствующей записи, а не с вопроса «что сломалось в доставке событий». Журнал доставки отвечает на вопрос «что мы получили». Он молчит о том, что до нас так и не дошло. Событие, которое не пришло, в журнале не оставляет следа, и искать его там бесполезно.

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

Почему журнал вебхуков не видит собственных дыр

Доставка события через HTTP устроена так, что у неё есть несколько мест, где оно может исчезнуть без шума. Приёмник мог быть недоступен дольше, чем поставщик пытается повторять. Приёмник мог ответить успехом и тут же упасть при обработке, не записав ничего в CRM. Правило в сценарии могло отфильтровать заказ по ошибке. Токен интеграции мог истечь, и запись в CRM отклонилась уже после того, как вебхук был «принят».

Документация Shopify прямо исходит из этой картины: повторные доставки возможны, неуспешные доставки повторяются, но не бесконечно, а для надёжности рекомендуется периодически сверяться через API (Shopify: Verify webhook deliveries). Отсюда практический вывод: расчёт на то, что поставщик будет стучаться, пока мы не откроем, — это надежда, а не механизм. Те же рекомендации советуют не делать долгую работу прямо в обработчике, а ставить событие в постоянную очередь; это снижает число потерь на нашей стороне, но не убирает их полностью. Сверка нужна в любом случае.

Заметим и обратную сторону: повторы тоже бывают. Заголовок X-Shopify-Webhook-Id помогает распознать повтор одного и того же события. Это важно для нашей темы, потому что сверка, которая что-то догружает, обязана жить в мире, где то же событие может прийти ещё раз, уже после того как мы закрыли дыру вручную.

Что с чем сопоставлять: ключ и бизнес-состояние

Первый шаг — решить, по какому полю заказ в магазине и сделка (или другой элемент) в CRM считаются одним и тем же. Хороший ключ обладает тремя свойствами: он назначен магазином, не меняется при редактировании и записан в CRM при создании. Номер заказа для покупателя, вроде «#1042», подходит хуже: он красивый, но у некоторых магазинов его формат можно настроить, а у менеджеров есть привычка дописывать к нему пометки. Внутренний числовой идентификатор заказа надёжнее. Удобно хранить его в отдельном пользовательском поле CRM и не смешивать с названием сделки.

Формат пользовательских полей в Битрикс24 нужно учитывать отдельно: поля конкретного типа сущности и их идентификаторы у каждого портала свои, и в документации crm.item.list это оговорено. В примерах ниже имя поля условное: ufCrm_SHOP_ORDER_ID. На вашем портале оно будет другим, и его точное имя стоит посмотреть в настройках типа элемента.

Второй слой сопоставления — статус. Совпадение ключа говорит лишь о том, что запись есть. Но заказ, оплаченный в магазине и висящий «в ожидании оплаты» в CRM, это тоже расхождение, и менеджер увидит его как жалобу клиента, а не как ошибку интеграции. Поэтому нужна небольшая таблица соответствия: какому состоянию заказа в магазине должна отвечать какая стадия в CRM. Её полезно составлять не по названиям, а по смыслу: «деньги получены», «передан в доставку», «отменён». Стадии CRM обычно детальнее, чем статусы магазина, и несколько стадий могут отвечать одному статусу. Сверка проверяет не равенство, а допустимость: стадия лежит в разрешённом наборе для этого статуса.

Окно сверки: зачем отступать от «сейчас»

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

Поэтому у окна две границы, и обе осмысленные. Нижняя уходит достаточно глубоко в прошлое, чтобы поймать события, которые поставщик уже перестал повторять, а наш приёмник пропустил. Верхняя отстоит от «сейчас» на величину обычной задержки появления данных плюс запас. Условный пример: если заказ в норме попадает в CRM за минуту, а в плохие минуты очередь разгребается за четверть часа, верхняя граница ставится на полчаса назад. Заказы моложе этого возраста не сравниваются вообще: их время ещё не пришло.

Есть и вторая оговорка. Выбирать окно по времени создания недостаточно: заказ, созданный вчера, мог сегодня поменять статус, и именно это изменение потерялось. Поэтому окно лучше строить по времени последнего изменения, если API магазина это допускает, а для надёжности периодически делать более глубокий проход по времени создания, например раз в сутки за несколько дней. Частая лёгкая сверка ловит свежие дыры, редкая глубокая добирает то, что проскочило.

  • Лёгкая: каждые 15–30 минут, окно по времени изменения, от получаса назад до нескольких часов.
  • Глубокая: раз в сутки, окно по времени создания за последние дни.

Цифры здесь иллюстративные: настоящие подбираются по вашим наблюдаемым задержкам, а не по примеру.

Первая страница — ещё не весь список

Теперь нужно получить сами списки. Тут подстерегает тихая ошибка: запрос вернул порцию записей, сравнение прошло гладко, расхождений нет, а на деле мы сравнили лишь начало. Постраничные API отдают данные частями, и сверка, которая не дочитала выдачу до конца, превращает «не увидели» в «нет расхождений». Это хуже, чем отсутствие сверки: она создаёт уверенность.

В Битрикс24 метод crm.item.list принимает параметры select (какие поля вернуть), filter (условия отбора), order (порядок) и start (с какой позиции продолжать). Когда есть следующая страница, ответ содержит значение next, и именно его нужно подставить в start для следующего запроса (Битрикс24: crm.item.list). Если next в ответе нет, страниц больше нет. Отсюда простое правило цикла: читать, пока next присутствует, и только тогда считать набор полным. Мерить полноту «по числу записей на странице» нельзя: неполная страница не всегда последняя.

Набросок тела запроса к CRM, где entityTypeId — тип элемента на вашем портале (значение подставляется своё):

{
  "entityTypeId": 2,
  "select": ["id", "stageId", "updatedTime", "ufCrm_SHOP_ORDER_ID"],
  "filter": {
    ">=updatedTime": "2026-11-01T00:00:00+03:00",
    "<=updatedTime": "2026-11-01T11:30:00+03:00"
  },
  "order": {"id": "ASC"},
  "start": 0
}

Здесь стоит сказать о каждом параметре отдельно, потому что от них зависит корректность. Select держит ответ лёгким: для сверки не нужны комментарии и контакты, нужны ключ, стадия и время изменения. Filter задаёт то самое окно из предыдущего раздела. Order фиксирует стабильный порядок: если сортировать по полю, которое меняется во время обхода, записи могут сдвинуться между страницами, и одна будет прочитана дважды, а другая пропущена. Сортировка по идентификатору в этом смысле спокойнее. Start начинается с нуля и дальше берётся из next.

Названия операторов фильтра и формат дат стоит сверить с документацией вашей версии; пример иллюстрирует форму запроса, а не обещает готовый набор для любого портала. То же верно для магазинной стороны: она читается своим постраничным механизмом, и принцип «дочитать до конца» остаётся тем же, каким бы ни был курсор.

Отдельная гигиена: если обход прервался на середине (таймаут, ограничение частоты), результат сверки нельзя считать полным. Такой прогон помечается как неполный, и по нему не делаются выводы об отсутствующих записях. Иначе половина заказов «потеряется» просто потому, что вторая половина не была загружена.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Пять видов расхождения и что с ними делать

Когда оба набора загружены целиком, их сопоставляют по ключу, и перед нами не одна категория «ошибка», а несколько разных историй. Разделять их стоит сразу, потому что лечатся они по-разному, а смешивание даёт лишние исправления.

Типы расхождений между магазином и CRM
СитуацияКак выглядитЧто делать
Заказ есть в магазине, в CRM нетКлюч из магазина не найден среди элементов CRM, возраст заказа больше порога задержкиПоставить на создание в CRM отдельной задачей
Заказ слишком свежийЗаписи в CRM нет, но заказ моложе порогаНе трогать, проверить в следующем прогоне
Состояния разошлисьКлюч найден, стадия вне допустимого набора для статусаОбновить существующий элемент, а не создавать новый
Запись есть только в CRMВ CRM ключ есть, в магазине такого заказа нет в окнеПроверить: удалён, тестовый или вне окна; автоматически не удалять
Дубль в CRMОдин ключ встречается у двух и более элементовОстановить автоматику по этому ключу, разобрать вручную

Первая строка — та самая «сто и девяносто девять». Вторая нужна, чтобы не путать пропажу с очередью, и её наличие в отчёте полезно: растущее число «слишком свежих» говорит, что задержка больше, чем мы считали, и порог пора пересматривать.

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

Четвёртая строка требует осторожности. Запись только в CRM может быть законной: сделка, созданная менеджером по телефону, или заказ другого канала. Автоудаление по такому признаку опасно. Сверка должна показывать и спрашивать, а не решать.

Пятая — дубли. Они не результат самой сверки, но она их находит, и поэтому по такому ключу автоматическое исправление лучше отключить: непонятно, какой из двух элементов настоящий, и любое действие рискует усугубить путаницу.

Чинить отдельно от сравнения

Здесь есть тонкий архитектурный выбор. Можно заставить сверку сразу создавать недостающие записи. Выглядит экономно, но смешивает две ответственности: что-то узнать и что-то изменить. Если сравнение ошиблось, изменение разнесёт ошибку по данным. Надёжнее разделить: сверка пишет список расхождений в таблицу или очередь, а исправление — отдельная повторяемая задача, которая берёт оттуда пункты по одному.

Главное требование к этой задаче — безопасность повторного запуска. Перед созданием элемента в CRM она заново ищет по ключу заказа: вдруг запись появилась, пока пункт лежал в очереди (например, запоздавший вебхук всё-таки дошёл). Если элемент найден, создание не нужно, остаётся при необходимости обновить стадию. Это поиск-потом-запись, и он закрывает гонку между вебхуком и сверкой. Строго говоря, между проверкой и записью остаётся маленькое окно, в которое теоретически может вклиниться тот же вебхук, поэтому после догрузки полезно ещё раз проверить, что по ключу ровно один элемент, а повтор сверки всё равно подстрахует. Не стоит ожидать, что внешний вызов CRM будет частью транзакции вашей собственной базы: подтверждённого механизма атомарности между ними нет, и согласованность обеспечивается повторными проходами, а не одной большой операцией.

У задачи исправления есть и ограничения на объём. Если за один прогон найдено три пропуска — это рутина. Если триста — вероятно, лежал приёмник или сломана авторизация, и массовое создание в CRM нужно сначала согласовать с человеком: иначе мы получим триста сделок, по которым менеджеры начнут звонить клиентам с опозданием на сутки. Порог, после которого задача останавливается и зовёт на помощь, — часть дизайна, а не перестраховка.

Запись о каждом исправлении стоит хранить: ключ заказа, тип расхождения, что сделано, когда. По этому журналу видно не только отдельные случаи, но и закономерности. Если пропуски группируются в одни и те же часы, причину надо искать в доставке, а не в сверке.

Где всё это живёт

Сверка — это небольшая, но регулярная нагрузка: расписание, два постраничных обхода, сопоставление, очередь исправлений. Ей нужно место, где процесс работает независимо от того, открыта ли чья-то вкладка, и где можно хранить промежуточные списки и журнал. Для таких задач подходит собственный сервер с root-доступом: там запускается планировщик, оркестратор вроде n8n или скрипт, а состояние сверки лежит рядом. У MAATRIX в каталоге есть VPS на NVMe с процессорами AMD EPYC, а в предложении перечислены n8n и Nextcloud с установкой; доступность конкретного образа и ОС проверяется в форме заказа.

По ресурсам разумно исходить из характера задачи. Лёгкий прогон по небольшому магазину не требует многого: для начала смотрят на тариф Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) или Uab (2 vCPU, 4 ГБ, 80 ГБ), если на той же машине работает оркестратор и хранится журнал. Если заказов много, сверка идёт часто и параллельно живут другие сценарии, посмотрите Heka и выше. Это ориентиры, а не обещание ёмкости: реальную нагрузку покажут замеры вашего расписания и объёма выборок. Расположение сервера (в предложении указаны UK, US, RU и FR) выбирают, исходя из того, где удобнее держать данные и как отвечают нужные API; гарантий доступности чужих сервисов из конкретной страны это не даёт.

Про обмен между самими сценариями — как сверять заказы и платежи в n8n и не обрабатывать одно и то же дважды — подробнее в статье «Сверка заказов и платежей в n8n»; там та же идея разобрана на другом инструменте.

Как выглядит один прогон целиком

Соберём прогон в одну историю. Расписание запускает лёгкую сверку. Она вычисляет окно: нижняя граница — несколько часов назад, верхняя — полчаса назад. Читает заказы магазина за окно, страница за страницей, пока есть продолжение. Читает элементы CRM с тем же окном, подставляя next в start, пока он приходит. Если на любом шаге обход оборвался, прогон помечается неполным, а в отчёте остаётся только запись об этом.

Затем два набора сопоставляются по ключу. Отсутствующие записи старше порога уходят в очередь на создание, свежие — в «подождать». Пары с несовпавшим состоянием попадают в очередь на обновление с учётом того, какая сторона главная по полю. Записи только из CRM и дубли формируют отчёт для человека. Задача исправления берёт пункты из очереди, перед каждым действием заново ищет элемент по ключу и только потом пишет. В конце счётчики расхождений по типам записываются, чтобы завтра можно было сравнить их с сегодняшними.

Эти счётчики сами становятся полезным индикатором. Ноль на протяжении недели — спокойная работа. Единичные пропуски — нормальный шум сетей. Внезапный рост «слишком свежих» — задержка выросла. Всплеск «нет в CRM» среди старых заказов — где-то ломалась доставка или авторизация, и теперь есть чёткий интервал времени, с которого начинать разбирательство.

Заказ, который нашёлся

Вернёмся к девяносто девяти заказам из ста. Прежде единственный ответ звучал так: «событие потерялось, значит, заказ потерялся». Теперь это не одно и то же. Событие могло исчезнуть по дороге, но состояние магазина никуда не делось, и ближайший прогон увидит ключ, которого нет в CRM, проверит возраст, дочитает выдачу до конца, поставит пункт в очередь и создаст недостающий элемент ровно один раз. Менеджер получит сделку чуть позже обычного, а не не получит вовсе, и на его стороне дыра окажется заметна лишь строчкой в журнале исправлений. Вебхук остаётся быстрым путём, а связь между двумя системами держится на том, что их можно в любой момент сравнить.