API для учётной системы

Передавайте себестоимость и остатки из 1С (или любой другой программы) прямо в SnappyCheck — без файлов и ручной загрузки. Ниже всё, что нужно программисту: адрес, ключ, готовые примеры запросов, правила записи, лимиты и коды ошибок.

Что это и зачем

Аналитика считает прибыль по себестоимости и показывает остатки. Оба числа живут в вашей учётной системе и меняются каждый день. Этот API нужен, чтобы они попадали в SnappyCheck сами.

Что умеет ключ: писать себестоимость и остаток по баркоду для ОДНОГО магазина и (если разрешено отдельной галочкой) читать записанные значения обратно. Всё.

Чего не умеет: ключей маркетплейсов, заказов, финансов, данных других магазинов он не видит и увидеть не может — это отдельный, намеренно узкий класс доступа.

Данные привязываются к баркоду (штрихкоду товара). Это единственный идентификатор, общий для всех площадок, поэтому себестоимость, загруженная один раз, работает и в Ozon, и в Wildberries, и в Lamoda.

Ключ и адрес

Базовый адрес: https://portal.snappycheck.ru/api/v1 — только https; на http:// сервер отвечает редиректом 308, а не данными. Сам по себе базовый адрес ничего не отдаёт (404) — обращайтесь к конкретным точкам: /ping, /status, /items.

Аутентификация: заголовок Authorization: Bearer sc_live_… в каждом запросе. Больше ничего не требуется — ни подписи, ни сессии.

Где взять ключ

  1. Владелец магазина заходит в портал → раздел «Интеграция» (portal.snappycheck.ru/integration). Раздел доступен администратору магазина или владельцу аккаунта.
  2. Придумывает имя ключа («1С основной») и нажимает «Создать ключ».
  3. Ключ показывается один раз, целиком. Потерян — восстановить нельзя, нужно выпустить новый и отозвать старый.

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

Две настройки ключа

  • «Разрешить читать значения» — галочка при создании. Без неё GET /api/v1/items отвечает 403: даже украденный ключ не выгрузит вашу себестоимость. Запись работает всегда.
  • «С каких IP принимать» — поле в строке ключа в портале: IP-адреса (через запятую), с которых ключ принимается; с любого другого адреса — 403 ip_not_allowed. Пусто — принимается отовсюду. Сравнение точное, по отдельным адресам; подсети (/24) не поддерживаются. Удобнее всего заполнить после первого удачного обмена: под полем появится адрес, с которого он пришёл, и кнопка «разрешить только его».
Ключ — это пароль к данным магазина. Не кладите его в git, в общие папки и в письма. Если ключ мог утечь — отзовите его в портале, обмен по нему прекращается сразу.

Проверка связи

GET/api/v1/ping — самый первый запрос, который стоит сделать. Ничего не меняет, проверяет ключ и показывает, к какому магазину он привязан.

Запрос
curl -s https://portal.snappycheck.ru/api/v1/ping \
  -H "Authorization: Bearer sc_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Ответ 200
{
  "store": "ООО Ромашка",
  "can_write": true,
  "can_read_values": false
}

store — название магазина: убедитесь, что это тот магазин, который вы ожидали. can_read_values — разрешено ли этому ключу читать значения (см. Читать значения).

Отправить данные

POST/api/v1/items — основной метод. Принимает пачку строк («партию») и записывает их магазину, к которому привязан ключ.

Запрос
curl -s -X POST https://portal.snappycheck.ru/api/v1/items \
  -H "Authorization: Bearer sc_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2026-08-05T09:00-delta" \
  -d '{
    "mode": "delta",
    "dry_run": false,
    "items": [
      {"barcode": "4600000000001", "cost_price": 512.40, "stock": 17},
      {"barcode": "4600000000002", "cost_price": 899},
      {"barcode": "4600000000003", "stock": 0}
    ]
  }'
Ответ 200
{
  "batch_id": "0f4a9d2e-6c1b-4f7a-9a3d-1f2b3c4d5e6f",
  "status": "applied",
  "duplicate": false,
  "received": 3,
  "applied": 2,
  "skipped_locked": 0,
  "unknown_barcodes": ["4600000000003"],
  "unknown_count": 1,
  "warnings": []
}

Поля запроса

ПолеТипЗначение
modeстрока "delta" (по умолчанию) — прислали только изменения; "full" — полная картина склада. См. Режимы.
dry_runда/нет false по умолчанию. true — посчитать результат, но НИЧЕГО не записывать. См. Предпросмотр.
itemsсписок Строки партии. Максимум 10 000 за запрос.
items[].barcodeстрока Обязательное. До 64 символов.
items[].cost_priceчисло Себестоимость единицы. Можно строкой ("512.40").
items[].stockчисло Остаток на вашем складе. Можно строкой. Ноль — валидное значение.

Поле, которого нет в строке, не трогается. Хотите обновить только остатки — присылайте строки вообще без cost_price.

Поля ответа

ПолеЗначение
batch_id Идентификатор партии. По нему её видно в портале, в истории обменов, и по нему делается откат.
status "applied" — записана; "dry_run" — это был предпросмотр, ничего не записано; "held" — сработал стоп-кран, партия ждёт решения владельца в портале.
duplicate true — эту партию уже принимали ранее по тому же Idempotency-Key, повторно ничего не записано (см. Идемпотентность).
receivedСколько строк пришло в теле запроса.
applied У скольких ТОВАРОВ реально сдвинулось хотя бы одно число. Прислали то же самое, что уже хранится, — будет 0. Это нормальный, здоровый результат синхронизации, а не ошибка.
skipped_locked Сколько цен не записано из-за замка себестоимости (см. Правила записи).
unknown_barcodes Баркоды, которые не приняты: их нет ни в каталоге магазина, ни в его себестоимости. Первые 100 штук — для диагностики. Значения по ним не сохраняются, пришлите их снова, когда товар появится в каталоге.
unknown_countСколько таких баркодов всего.
warnings Список человекочитаемых предупреждений по-русски: какие строки отброшены и почему, что выглядит подозрительно. Логируйте его. Партия при этом принята.

Правила записи

Часть правил неочевидна, и от них зависит, не потеряете ли вы данные. Прочитайте эту таблицу до того, как настроите выгрузку.

Что прислалиЧто произойдёт
cost_price больше нуляСебестоимость записывается.
cost_price: 0, null или поля нет Ничего не меняется. Обнулить себестоимость через API нельзя в принципе — ноль в выгрузке почти всегда означает «данных нет», а не «товар бесплатный». Обнулить можно только руками в портале.
stock: 0 Записывается ноль. «На складе пусто» — это факт, а не отсутствие данных.
stock: null или поля нетОстаток не трогается.
Отрицательное значение в любом поле Строка отбрасывается целиком, в warnings — номер строки. Остальная партия применяется.
Пустой баркод или длиннее 64 символов Строка отбрасывается, в warnings — номер строки. Обрезать баркод нельзя: обрезок склеил бы разные товары.
Один баркод встречается дважды Строки схлопываются поле за полем: если второе вхождение принесло только stock, ранее собранная cost_price сохраняется, а не затирается пустотой.
Баркод, которого нет ни в каталоге, ни в себестоимости Строка не принимается целиком — ни цена, ни остаток; баркод уходит в unknown_barcodes. Каталог наполняется из кабинетов маркетплейсов ночью, поэтому только что заведённый товар может доехать не сразу: пришлите его в следующей выгрузке. Если в режиме full не принята ни одна строка, партия отбивается ошибкой 422 (empty_full_batch) — иначе она обнулила бы остатки по всему складу.
Товар с замком себестоимости Цена НЕ записывается (замок ставит продавец в портале — «эту цену веду руками»), счётчик skipped_locked растёт. Остаток при этом пишется как обычно — замок только про цену.
Числа можно присылать строками ("512.40") — учётные системы часто так и делают. Разделитель — точка.

Режимы delta и full

delta — «вот что изменилось»

Режим по умолчанию. Трогаются только присланные баркоды, остальные строки магазина не задеваются вообще. Это правильный режим для регулярной выгрузки каждые несколько часов.

full — «вот вся картина склада»

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

Что не обнуляется:

  • себестоимость — никогда, режим касается только остатков;
  • строки, которых интеграция ещё ни разу не касалась (продавец завёл их руками в портале) — «полная картина» вашего склада про них ничего не утверждает.
Полная выгрузка склада
curl -s -X POST https://portal.snappycheck.ru/api/v1/items \
  -H "Authorization: Bearer sc_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: stock-2026-08-05" \
  -d '{"mode": "full", "items": [
        {"barcode": "4600000000001", "stock": 17},
        {"barcode": "4600000000002", "stock": 4}
      ]}'
Не отправляйте full, если не уверены, что выгрузка полная. Оборванный или отфильтрованный запрос в 1С — самая частая причина обнуления склада. Против этого работают две защиты: если full не принесла ни одной годной строки, вы получите 422 и ничего не запишется; если она обнулила бы больше половины склада — сработает стоп-кран.

Предпросмотр (dry_run)

"dry_run": true прогоняет партию целиком — разбор, правила записи, замки, все проверки, — и возвращает те же счётчики, что вернул бы боевой вызов, но не записывает ничего. Ответ приходит со "status": "dry_run".

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

Предпросмотр не занимает ключ идемпотентности: тот же Idempotency-Key потом сработает по-настоящему.

Повторы и идемпотентность

Сеть рвётся, задача падает по таймауту, cron запускается дважды. Чтобы повтор не записал партию второй раз, присылайте заголовок Idempotency-Key — любую строку до 64 символов, уникальную для этой партии (например, «2026-08-05T09:00-delta»).

Если партия с таким ключом уже принималась у этого магазина, ответ придёт 200 с "duplicate": true и счётчиками ПЕРВОЙ партии — ничего не записывается повторно.

Ключ длиннее 64 символов — это 400 (idempotency_key_too_long), партия не принимается. Обрезать его молча нельзя: два разных ключа с одинаковым началом слиплись бы в один, и вторая партия тихо стала бы «дублем» первой.

Без этого заголовка каждый запрос считается новой партией.

Читать значения

GET/api/v1/items?barcodes=a,b,c — вернуть то, что сейчас хранится. Работает только у ключа, которому при создании разрешили чтение (галочка в портале), иначе 403 read_not_allowed.

Запрос
curl -s -G https://portal.snappycheck.ru/api/v1/items \
  -H "Authorization: Bearer sc_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
  --data-urlencode "barcodes=4600000000001,4600000000002"
Ответ 200
{
  "4600000000001": {"cost_price": 512.4, "stock": 17.0, "locked": false},
  "4600000000002": {"cost_price": 899.0, "stock": null, "locked": true}
}

Баркоды, которых нет в базе магазина, просто отсутствуют в ответе — это не ошибка. locked: true означает замок себестоимости: цену этого товара ваши партии не перезапишут.

За один запрос принимается до 1000 баркодов; лишние молча отбрасываются — режьте список на своей стороне. Пустой barcodes — пустой ответ {}.

Состояние обмена

GET/api/v1/status — короткая сводка по магазину. Удобно для мониторинга на вашей стороне: например, поднять тревогу, если last_batch_at старше суток.

Ответ 200
{
  "last_batch_at": "2026-08-05T09:00:11.482913+00:00",
  "last_batch_status": "applied",
  "items_under_integration": 1840,
  "locked_items": 3
}
ПолеЗначение
last_batch_at Время последней партии магазина (UTC, ISO 8601). null — обменов ещё не было.
last_batch_status Её статус: applied, dry_run, held, rejected, rolled_back.
items_under_integration Сколько товаров магазина ведёт интеграция (хотя бы одно поле записано ею).
locked_items Сколько товаров с замком себестоимости.

Лимиты и частота

ОграничениеЗначениеЧто будет при превышении
Строк в одной партии10 000 413 too_many_items — режьте выгрузку на части
Размер тела запроса4 МБ 413 body_too_large
Частота записи1 обмен в 10 секунд на ключ 429 rate_limited + заголовок Retry-After: 10
Длина Idempotency-Key64 символа 400 idempotency_key_too_long
Длина баркода64 символа отбрасывается только эта строка, с предупреждением
Баркодов в одном чтении1000лишние отбрасываются молча
Товарных позиций на магазин500 000 новые баркоды сверх потолка не заводятся (предупреждение в warnings), обновления уже известных проходят как обычно
Хранение истории партий30 дней старые партии удаляются, откат по ним больше недоступен
Троттлинг считается только по ЗАПИСИ (POST /items). ping, status и чтение значений его не занимают и им не ограничены. Отметка «был обмен» ставится ДО обработки партии, поэтому неудачная партия тоже занимает окно — сломанный цикл не сможет долбить сервер отказами.

Как часто присылать. Раз в 10 секунд — это потолок защиты, а не рекомендация. Реальной аналитике достаточно обмена раз в час; полную выгрузку (full) обычно делают раз в сутки.

Коды ошибок

Тело любой ошибки этого API выглядит так:

{
  "error": "неверный ключ",
  "code": "invalid_key"
}

error — человеку в лог. code — программе. Разбирайте именно code: текст error мы можем переформулировать в любой момент, а code стабилен. У некоторых ошибок в теле есть дополнительные поля — они указаны в таблице.

HTTPcodeЧто случилось и что делать
401invalid_key Ключа нет в заголовке, он неверный или отозван. Проверьте заголовок Authorization и живой ли ключ в портале.
403ip_not_allowed Запрос пришёл с адреса, которого нет в списке разрешённых для этого ключа. Поправьте список в портале.
403no_active_access У магазина нет ни одного модуля с активным оплаченным периодом (или сам магазин выключен). Обмен возобновится после оплаты, менять ничего в коде не нужно.
403read_not_allowed Ключу не разрешено читать значения. Разрешается галочкой при создании ключа — у выпущенного ключа её не поменять, нужен новый ключ.
400invalid_mode mode — только delta или full.
400idempotency_key_too_long Idempotency-Key длиннее 64 символов.
413too_many_items Строк больше 10 000. В теле поле limit — потолок в СТРОКАХ.
413body_too_large Тело больше 4 МБ. В теле поле limit — потолок в БАЙТАХ.
429rate_limited Чаще одного обмена в 10 секунд. Заголовок Retry-After говорит, через сколько секунд повторить.
422empty_full_batch mode=full не принёс ни одной годной строки — так обнулился бы остаток всего склада, поэтому партия отвергнута целиком и НИЧЕГО не записано. В теле warnings — почему строки отброшены.
422held_batch_pending Предыдущая полная выгрузка задержана стоп-краном и ждёт решения владельца в портале. Новые full не принимаются, пока он не решит. delta работает как обычно.
422invalid_body Тело не разобрано: не JSON, не тот тип поля. В теле detail — что именно не понравилось.
Все ответы 2xx поля code НЕ содержат — оно есть только у ошибок. Ориентируйтесь на HTTP-статус, а внутри статуса — на code: под одним статусом у нас живут разные причины.

Стоп-кран: задержанная партия

Если mode=full обнулила бы остаток больше чем у половины товаров под интеграцией (и таких товаров не меньше 20), партия не применяется. Ответ — обычный 200, но со "status": "held": партия принята и сохранена целиком, данные магазина не тронуты.

Решает человек: владелец магазина видит партию в портале, в разделе «Интеграция», и нажимает «Применить» (записать ровно то, что показано) или «Отклонить» (данные остаются как есть).

Пока решения нет, каждая следующая full (в том числе с dry_run) получает 422 held_batch_pending. Режим delta продолжает работать без ограничений.

Что делать в коде: получили "status": "held" — не повторяйте отправку, а сообщите оператору, что полная выгрузка ждёт подтверждения в портале. Повторы всё равно будут отвергнуты, а склад изменится только после решения человека.

Резкий скачок и откат

Если больше чем у 20 % сравнимых строк себестоимость изменилась более чем втрое, партия применяется, но помечается как подозрительная: в warnings появляется предупреждение с числами, а в портале — плашка со ссылкой на откат. Сравнимыми считаются строки, у которых уже была прежняя цена (первая в жизни загрузка аномалией не выглядит).

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

Отзыв ключа

Ключ перестаёт работать в трёх случаях. После отзыва любой запрос по нему — 401 invalid_key; восстановить отозванный ключ нельзя, только выпустить новый.

КогдаПричина
СразуВладелец нажал «Отозвать» в портале.
Через 30 дней Ключом ни разу не воспользовались с момента выпуска. Выпустили «на потом» — он не доживёт до «потом».
Через 90 дней Ключ молчит: ни одного успешного запроса. Опасны не старые ключи, а забытые — выданный подрядчику на разовую настройку ключ остаётся жить в чужом ноутбуке.

Оба автоматических отзыва предупреждаются за 7 дней — плашкой в портале, с точным числом оставшихся дней.

«Воспользовались» — это ЛЮБОЙ успешный запрос, включая ping, status и чтение. Ключ «только на чтение», которым пользуются каждый день, отозван не будет.

Частые ошибки

Собрано из реальных обращений. Если ваша ситуация здесь — исправление займёт минуту.

Приходит код 308 (Permanent Redirect), тела нет

Запрос ушёл на http://. Сервер работает только по https и на обычный http отвечает «переезжайте на https» (308). Браузер такой редирект проходит сам, а HTTP-клиент 1С — нет, поэтому вы видите 308. Исправление: адрес https://portal.snappycheck.ru, порт 443, защищённое соединение.

Соединение = Новый HTTPСоединение("portal.snappycheck.ru", 443, , , , , Новый ЗащищенноеСоединениеOpenSSL());
Запрос = Новый HTTPЗапрос("/api/v1/ping");
Запрос.Заголовки.Вставить("Authorization", "Bearer sc_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
Ответ = Соединение.Получить(Запрос);
Сообщить(Ответ.КодСостояния);            // ждём 200
Сообщить(Ответ.ПолучитьТелоКакСтроку()); // {"store": "…", "can_write": true, …}

В ответе {"detail": "Not Found"} (404)

Такого адреса нет. Чаще всего открывают «корень» /api/v1 или /api/v1/ — сам по себе он ничего не отдаёт. Рабочие адреса только три: GET /api/v1/ping, GET /api/v1/status, POST/GET /api/v1/items. Проверьте путь по буквам: /api/v1/item, /api/v1/ping/ с лишним слешем или /api/ping без v1 — тоже 404.

Открываю адрес в браузере — «неверный ключ» (401)

Так и должно быть: браузер из адресной строки не передаёт заголовок с ключом, а без него любой запрос — 401. Браузер для проверки не годится. Проверяйте из командной строки — одной строкой:

curl -s https://portal.snappycheck.ru/api/v1/ping   -H "Authorization: Bearer sc_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Ответ вида {"store": "Название магазина", "can_write": true, …} — связь есть, ключ верный. Тот же 401 из 1С означает: заголовок не дошёл (опечатка в слове Authorization, нет слова Bearer и пробела после него, лишние пробелы или перенос строки в ключе) либо ключ отозван в портале.

403 ip_not_allowed — «адрес не разрешён для этого ключа»

У ключа в портале заполнено поле «С каких IP принимать», и запрос пришёл с другого адреса (например, сервер 1С переехал или выгрузку запустили с рабочего места). Владелец магазина либо добавляет новый адрес в это поле, либо очищает его — тогда ключ снова принимается отовсюду.

403 read_not_allowed при чтении

Ключ выпущен без галочки «разрешить читать значения» — писать он может, читать нет. Для сверки нужен ключ с этой галочкой (выпускается заново, старый можно отозвать).

Как понять, что запрос вообще дошёл до SnappyCheck

В портале, в разделе «Интеграция», у ключа есть колонка «Использован» — время последнего запроса с этим ключом — и под полем адресов подпись «последний обмен пришёл с адреса …». Если там «ни разу», до нас не долетело ни одного запроса: смотрите два первых пункта (http вместо https, неверный адрес).

Поддержка

Вопрос по интеграции, нужен пример под вашу конфигурацию 1С или кажется, что сервис ответил неправильно — напишите нам и приложите batch_id из ответа: по нему видно всё, что произошло с партией.

Почта: SnappyCheck@yandex.com

Ещё не подключили магазин? Аналитика Ozon, Wildberries и Lamoda — бесплатно на 30 дней, без карты.

Попробовать бесплатно