Как подружить 1С и Shopify: обмен ценами и остатками на практике
от v2Team
Один из клиентов вёл ассортимент и остатки в 1С, а витрина у него — на Shopify. Задача звучала просто: цены и остатки, которые бухгалтерия и склад видят в 1С, должны автоматически появляться на сайте. На практике это оказался классический интеграционный проект, где большая часть сложности была не в «как дёрнуть API», а в деталях: как не положить процесс 1С на часы ожидания, как не перепутать валюты, как не удвоить остаток от склада брака и что делать, когда Shopify говорит «такого товара нет», хотя вчера он там был.
По смете у нас был согласован «Вариант 1»: только цены и остатки, простые товары без характеристик, один тип цены, соответствие товаров задаётся заранее. Создание карточек товаров, изображений, обмен заказами — вне рамок этой задачи. Ниже — как мы это построили, с какими граблями столкнулись и что показали логи уже в проде.
Нулевой шаг: приложение и ключи доступа в Shopify
Прежде чем писать код, нужно завести на стороне Shopify «Custom App» — без него сервис на сервере просто не сможет достучаться до Admin API.
- В админке магазина: Settings → Apps and sales channels → Develop apps. Если раздел недоступен, сначала нужно включить Allow custom app development — по умолчанию эта возможность выключена.
- Create an app, дать понятное имя (у нас — по имени интеграции, не «test» и не «app1», чтобы через полгода не гадать, что это и зачем).
- Во вкладке Configuration выдать Admin API именно те scopes, которые реально нужны, а не всё подряд:
write_products,read_products,write_inventory,read_inventory,read_locations. Права на заказы, клиентов, скидки и т.д. интеграции не нужны — и лучше их не запрашивать: чем уже права токена, тем меньше цена ошибки, если он утечёт. - Install app на магазин — после этого Shopify один-единственный раз показывает Admin API access token (
shpat_...). Это тот самый момент, который легко упустить: если не скопировать токен сразу, второй раз посмотреть его нельзя — придётся отзывать и перевыпускать заново. - Отдельно нужен Location ID — он не выдаётся вместе с токеном, а достаётся либо в Settings → Locations (в конце URL конкретной локации), либо простым GraphQL-запросом
{ locations(first: 10) { nodes { id name } } }. Без него не получится обновлять остатки: мутацияinventorySetQuantitiesтребует локацию явно, Shopify не пытается угадывать её сама. - И последнее — версия API. Всё обращение в Shopify идёт по адресу вида
https://{shop}.myshopify.com/admin/api/{version}/graphql.json, и версию стоит зафиксировать (например,2025-01), а не полагаться на «последнюю» — Shopify выпускает новые версии API раз в квартал, и часть полей/мутаций между версиями меняется. Плавающая версия — источник поломки в день релиза, который никак не связан с изменениями в вашем коде.
Итог этого шага — четыре значения в конфиге: домен магазина, access token, ID локации и зафиксированная версия API. Дальше уже можно писать интеграцию.
Отправная точка: у 1С уже есть протокол, не нужно его выдумывать
1С умеет выгружать данные на сайт по стандартному протоколу обмена CommerceML — тому же, что используется в модуле «Обмен с сайтом» для 1С-Битрикс. Это большой плюс: не нужно придумывать свой формат и просить программиста 1С писать нестандартную выгрузку. Достаточно реализовать на своей стороне четыре обязательных шага, которые ожидает 1С:
- проверка подключения — 1С стучится с логином и паролем, сервер отвечает токеном сессии
- инициализация — сервер сообщает ограничения (максимальный размер файла, поддержку архивов)
- приём файлов — 1С заливает XML-файлы по одному, сервер просто сохраняет их на диск
- импорт — сервер получает команду «разбери файл X» и обрабатывает его
Важная деталь, в которую легко не поверить заранее: 1С не присылает всё одним огромным файлом. Она режет выгрузку на десятки, а то и сотни файлов за одну сессию — отдельно каталог товаров, отдельно цены, отдельно остатки, и всё это может дробиться на порции. Обработчик на стороне сайта должен уметь понять, что именно перед ним — иначе он в лучшем случае проигнорирует файл, а в худшем — попытается прочитать его не в том формате.
Первая развилка: синхронно или в очередь
Самое интуитивное решение — при получении файла с ценами сразу же идти в Shopify и обновлять товары один за другим, пока 1С ждёт ответа. Это работает на тестовых данных и разваливается на реальном объёме.
Причина простая: у внешнего API есть задержка сети на каждый вызов, а вызовов может быть тысячи. Если пытаться сделать всё синхронно, 1С будет физически ждать API-вызовы сайта несколько минут на каждый файл — и так на каждый из сотен файлов за сессию.
Мы разделили приём данных и их применение. exchange1c.php, получив файл от 1С, только складывает изменения в очередь (sync_queue) и сразу отвечает «принято» — сама отправка в Shopify внутри этого запроса не выполняется. Реальная синхронизация делается отдельным скриптом process_queue.php по cron раз в минуту. Это меняет всё: 1С отдаёт файлы за секунды, а не часы, а скорость применения данных зависит только от пропускной способности API магазина.
Отдельно пришлось защитить сам cron-скрипт файловой блокировкой: если импорт большого каталога привёл к скачку очереди, предыдущий прогон process_queue.php может не уложиться в минуту, и без блокировки запустился бы второй процесс поверх первого. В логе в проде это и правда случалось — во время тестирования были минуты, когда обработка одной пачки занимала дольше 60 секунд, и лишний запуск просто выходил без работы:
[2026-08-14 12:37:02] предыдущий запуск ещё выполняется, пропуск
[2026-08-14 12:38:44] done=541 error=202 skipped=0
Вторая развилка: сопоставление товаров
1С и Shopify — две независимые системы, у них нет общего внутреннего идентификатора товара. Нужно построить таблицу соответствий: какой код товара в 1С соответствует какому товару в Shopify. Есть два естественных ключа для такого сопоставления — артикул (SKU) и название.
Название — ненадёжный ключ. Оно может отличаться на один пробел, регистр, порядок слов, и точное совпадение по названию — скорее исключение. Артикул надёжнее, если он вообще заполнен и уникален с обеих сторон, что на практике не всегда так: встречаются товары без артикула вовсе, и — что менее очевидно — встречаются дублирующиеся артикулы прямо в самом магазине, когда одна и та же карточка товара случайно заведена дважды. Дважды заведённый товар — это уже не техническая проблема выгрузки, а вопрос гигиены каталога: система должна такие случаи не сопоставлять вслепую, а показать администратору «вот тут неоднозначность, разбирайтесь сами» (у нас это отдельный экран admin/match.php).
Практичная стратегия — комбинированная: сначала пробовать сопоставить по артикулу, и только если это не удалось — пробовать по названию. При этом важно не дать одному и тому же товару в Shopify «достаться» двум разным товарам 1С за один проход — иначе сопоставление по названию может случайно перехватить чужой, уже правильно определённый по артикулу, товар. В коде это одна строчка — при поиске кандидатов по названию исключаются варианты, уже занятые кем-то другим за этот же прогон или раньше — но без неё расследовать «почему у нас на сайте не тот товар» пришлось бы вручную и постфактум.
Третья развилка: какая цена и какой склад — правильные
У одной позиции в 1С обычно несколько цен: розничная, оптовая, в разных валютах. Наивный подход — взять первую цену, которая попадётся в файле; порядок цен в выгрузке не гарантирован, и можно случайно записать цену в одной валюте как будто это цена в другой. Мы сделали выбор типа цены настройкой в админке (admin/settings.php) — 1С в файле классификатора присылает список всех типов цен, и это просто выпадающий список с радио-кнопками, а не хардкод.
Точно так же поступили с остатками. У товара в 1С может быть несколько складов, и «остаток на сайте» — это не автоматически сумма по всем: среди складов встречаются склад брака, склад списания, склад упаковочных материалов, продавать с которых нельзя. Список складов, которые суммируются в остаток, — тоже настройка, а не зашитое в код предположение «все склады = все продажи», и в интерфейсе он показан человекочитаемыми названиями складов, а не идентификаторами.
Четвёртая развилка: батчинг и его цена
У Shopify Admin API есть лимиты на количество запросов в единицу времени, и на каталоге в несколько тысяч позиций один запрос — одно обновление быстро упирается в это ограничение. Решение — группировать: цены обновляются мутацией productVariantsBulkUpdate (пачками по товару), остатки — inventorySetQuantities пачками по 200 позиций. Плюс на HTTP 429 (превышен лимит) клиент делает одну повторную попытку с секундной паузой — это отдельная, более частая причина сбоя одного вызова, чем логическая ошибка в данных.
Но у батчинга есть неочевидная цена: если Shopify не может исполнить всю пачку и возвращает одну общую ошибку на весь вызов, непонятно, какая именно позиция в пачке была проблемной. Мы увидели это не в теории, а в проде — на второй день после запуска, во время первой большой синхронизации остатков по всему каталогу, лог process_queue.php показал:
[2026-08-14 12:51:12] done=2892 error=1821 skipped=0
[2026-08-14 13:05:33] done=4598 error=402 skipped=0
1821 ошибка за один прогон — это не 1821 сломанный товар, это несколько «упавших» партий, каждая из которых утащила за собой всех соседей по батчу. Поэтому обработка сделана в два уровня: в штатном режиме работает быстрый групповой вызов, но если партия падает целиком, мы на этот случай переигрываем её поэлементно — находим настоящего виновника и не обвиняем всех рядом. Это заметно медленнее, но происходит только для упавших партий, а не для всего потока — именно поэтому число ошибок в следующих прогонах быстро падает почти до нуля, как только переигранные позиции проходят по одной.
Пятая развилка: самовосстановление или ручная работа
В процессе интеграции неизбежно возникают ситуации, когда сопоставление, построенное вчера, сегодня уже не годится: товар удалили в Shopify, объединили дубликаты, что-то переименовали. Мы завязали это на текст ошибки Shopify — если API отвечает could not be found или does not exist, значит вариант или товар за этим сопоставлением реально пропал, и его нужно сбросить, а не пытаться повторять бесконечно. В нашей базе за первые дни это видно прямо в таблице очереди — по коду 1С без действующего сопоставления фиксируется понятная причина:
Нет соответствия в таблице маппинга для кода 1С
Мы пошли на шаг дальше банального сброса: если в этот момент уже есть под рукой свежая выгрузка каталога Shopify (например, только что обновлялась для другого файла), процессор очереди сразу же пробует найти новое сопоставление по тому же артикулу, не дожидаясь следующего файла каталога от 1С или ручного запуска пересопоставления. Сценарий «в магазине почистили дубликаты карточек» в большинстве случаев закрывается автоматически в течение одной минуты — до следующего тика cron.
Шестая развилка: наблюдаемость
Интеграция, которая работает без интерфейса для человека, рано или поздно превращается в чёрный ящик, за который страшно отвечать. «Сколько товаров обновилось», «какие товары не нашли пару и почему», «идёт ли обмен прямо сейчас» — вопросы, на которые нужно уметь ответить за десять секунд, а не лезть в базу руками. У нас это admin/logs.php (журнал обмена и состояние очереди) и admin/dashboard.php — защищённый раздел, а не открытый всем.
Отдельного внимания стоит формулировка причин, по которым что-то не сопоставилось. Технически верный, но бесполезный для человека статус вроде «требует разбора» ничего не говорит о том, что делать дальше. Гораздо полезнее конкретика: «нет соответствия в таблице маппинга», «совпадение найдётся само при следующей синхронизации», «у товара вообще нет артикула — сопоставьте вручную». И отдельно — строки, которые не удалось обработать 5 раз подряд, не долбятся дальше автоматически, а помечаются для ручного разбора: бесконечный повтор заведомо мёртвой задачи только шумит в логах и не приближает к решению.
Что в итоге
Сдали именно «Вариант 1» по смете — простые товары без характеристик, один тип цены, один магазин, обмен только ценами и остатками, без заказов. Даже в этом сознательно суженном объёме основная сложность оказалась не в вызове Shopify API как такового, а в вопросах данных и надёжности: какая цена правильная, какой склад считается «на складе», что делать, когда одна и та же связка ломается у сотни товаров разом, как быстро распространить изменение, не удушив систему собственными же оптимизациями. Реальные логи первых дней синхронизации это подтвердили — всплески ошибок были не из-за багов в логике, а из-за самой природы батчинга и лимитов API, и система сама привела очередь в порядок за несколько прогонов cron. Все эти решения стоит закладывать в архитектуру с самого начала: честная система логов и понятные статусы сильно облегчают жизнь и разработчику, и человеку, который потом смотрит на дашборд и должен понять, всё ли в порядке.
Важно понимать, что «Вариант 1» — это осознанно суженный объём под конкретную задачу клиента, а не потолок возможностей связки 1С и Shopify. Тот же протокол CommerceML и та же очередь на обработку легко расширяются дальше: выгрузку и создание новых карточек товаров (с характеристиками, галереями изображений, категориями), обмен заказами в обратную сторону (из Shopify в 1С — резервирование, статусы отгрузки), работу с несколькими магазинами или валютами одновременно. Мы сознательно не стали проектировать это заранее «про запас» — но архитектура (разделение приёма и применения данных через очередь, таблица сопоставлений, наблюдаемость через логи) уже рассчитана так, что такие доработки — это расширение, а не переделка с нуля.
Нужна помощь с похожей задачей?
Расскажите о проекте — оценим, предложим решение и назовём стоимость. Без обязательств.
Читайте также
Поработаем?
Опишите свой запрос, мы рассчитаем стоимость вашей задачи.

