Сообщения партнёру (вебхуки)
Как магазин сам сообщает партнёру о заказах, ценах и остатках: включение в форме ключа, события, подпись, ответы, повторы и выключение отправки.
На этой странице
- Как это работает
- Как включить сообщения
- Адрес для сообщений
- О чём сообщать
- Секрет подписи
- Проверка
- Состояние отправки
- Журнал отправок
- Если передать ключ другому контакту
- Сообщение
- Заголовки
- Тело
- События
- order.status_changed
- order.updated
- prices.changed
- inventory.changed
- Если артикула нет в ответе
- webhook.test
- Проверка подписи
- PHP
- Node.js
- Проверочный пример
- Как отвечать
- Повторы и выключение отправки
- Порядок и повторы сообщений
- Чего нет
Обычно партнёр сам раз в несколько минут спрашивает магазин, что изменилось, и забирает заказы, цены и остатки через API. Сообщения партнёру работают наоборот. Магазин сам отправляет запрос на адрес партнёра, когда у заказа сменился статус, менеджер поправил заказ, цены могли стать другими или изменились остатки. Разработчики называют такие сообщения вебхуками.
Самих данных в сообщении нет. В нём только название события и номер заказа или номера артикулов. Свежие данные партнёр забирает через API обычными запросами, с теми же правами ключа. Поэтому сообщение не открывает партнёру ничего сверх того, что ему и так видно в API.
Сообщения включает и настраивает владелец магазина в форме ключа. Через API их настроить нельзя.
Как это работает
- Владелец включает сообщения в форме ключа, вписывает адрес на сайте партнёра, отмечает события и передаёт партнёру секрет подписи.
- Когда случается отмеченное событие, магазин записывает сообщение и
отправляет его запросом
POSTна адрес партнёра. - Сайт партнёра проверяет подпись, отвечает кодом 2xx и забирает свежие данные через API.
- Если сайт партнёра не ответил или ответил ошибкой, магазин повторяет отправку по графику, а владелец видит каждую попытку в журнале отправок.
О заказах магазин сообщает сразу после сохранения заказа, без накопления. Сообщение встаёт в очередь отправки и обычно доходит в течение минуты. Изменения цен и остатков магазин собирает и раз в минуту отправляет одной пачкой, поэтому продажа сотни позиций даёт одно сообщение, а не сотню. Такое сообщение приходит в течение одной-двух минут после изменения.
Сообщения не заменяют сверку через API. Если адрес партнёра долго не отвечал, магазин выключает отправку, и часть сообщений не доходит. Кроме того, некоторые дополнения цен не сообщают магазину о своих правках. Поэтому полную сверку оставьте, только делайте её реже, например раз в сутки.
Как включить сообщения
Сообщения можно настроить только у выданного ключа. Откройте «Настройки платформы», в группе «Платформа» выберите «API магазина» и нажмите на ключ в списке. Блок «Сообщения партнёру» стоит в форме ключа после блоков «Каталог», «Заказы партнёра» и «Остатки». У нового ключа он появится после нажатия «Выдать ключ». Как выдать ключ, описано на странице «Ключи доступа и права».
Под заголовком блока написано: «Магазин сам сообщает партнёру об изменениях, и ему не нужно постоянно проверять их через API. В сообщении только номера заказов или артикулов, а сами данные партнёр забирает через API. Разработчики называют такие сообщения вебхуками.»
- Включите переключатель «Отправлять сообщения». Подсказка под ним: «О заказах магазин сообщает сразу, а изменения цен и остатков собирает и отправляет раз в минуту.»
- В поле «Адрес для сообщений» впишите адрес, который дал партнёр. Требования к адресу описаны ниже.
- В блоке «О чём сообщать» отметьте нужные события.
- Нажмите «Сохранить» в шапке ключа.
- Нажмите «Показать» или «Скопировать» у секрета подписи и передайте секрет партнёру.
- Нажмите «Отправить проверку» и убедитесь, что партнёр её принял.
Раздел ключей и все кнопки блока доступны сотрудникам с правом «Выдавать ключи API».
Адрес для сообщений
Подсказка под полем: «Адрес на сайте партнёра, который начинается с https://. Его даёт сам партнёр.»
Магазин принимает адрес, только если:
- он начинается с
https://. Адресаhttp://не подходят; - в нём нет своего порта. Порт можно не указывать, а если он указан, то только
:443. Адрес видаhttps://partner.example.ua:8443/hookмагазин не примет; - он ведёт на сайт в интернете. Магазин должен найти имя сайта в интернете, и
все IP-адреса этого имени, IPv4 и IPv6, должны быть внешними. Адреса
внутренней сети (
localhost,127.0.0.1,10.…,192.168.…,172.16.…, служебные адреса облаков и хостингов) не подходят, даже если у имени сайта такой адрес только один из нескольких.
Адрес на кириллице, например https://партнер.укр/hook, тоже подходит.
Если адрес не подходит, под полем появится «Адрес должен начинаться с https:// и вести на сайт в интернете. Номер порта, например :8080, в адресе указывать нельзя, а адреса внутренней сети не подходят.» Если включить отправку с пустым адресом, магазин ответит «Впишите адрес, на который магазин будет отправлять сообщения.»
Магазин проверяет адрес и перед каждой отправкой. Если магазин больше не находит имя сайта или оно стало вести во внутреннюю сеть, попытка считается неудачной, а в журнале будет написано «Магазин не нашёл этот адрес в интернете или адрес ведёт во внутреннюю сеть, поэтому сообщение не отправлено.» Магазин не переходит по перенаправлениям, поэтому ответ 301 или 302 считается неудачей.
Адрес у ключа один. Если сообщения нужны двум партнёрам, выдайте каждому свой ключ.
О чём сообщать
Каждое событие можно отметить, только если у ключа есть право видеть эти данные через API.
| Флажок | Подсказка под флажком | Событие | Нужное право |
|---|---|---|---|
| «Сменился статус заказа» | «Только по заказам контакта, которому выдан ключ.» | order.status_changed |
«Видеть заказы» (orders:read) |
| «Изменения в заказе» | «Когда в заказе меняется состав, суммы, покупатель, получатель, доставка, оплата, номер накладной или комментарий.» | order.updated |
«Видеть заказы» (orders:read) |
| «Могли измениться цены» | «Магазин сообщает, у каких товаров цена могла стать другой. Иногда цена при этом не меняется.» | prices.changed |
«Видеть цены» (prices:read) |
| «Изменились остатки» | «Только по складам, выбранным для этого ключа. Если склады не выбраны, по тем, с которых продаёт витрина.» | inventory.changed |
«Видеть остатки» (inventory:read) |
Флажок события без нужного права отметить нельзя, а под ним написано, например, «Сначала отметьте «Видеть заказы» в блоке «Что разрешено».» Если снять право в блоке «Что разрешено», форма сама снимает события, которые от него зависят. Если всё же отправить событие без права, магазин не сохранит ключ и ответит «Чтобы партнёр получал это сообщение, отметьте «Видеть заказы» в блоке «Что разрешено».»
Права магазин проверяет и перед каждой отправкой. Если у ключа больше нет права или событие больше не отмечено, сообщение не уходит, а в журнале у него написано «Отменено: ключу эти данные больше не видны».
Если отправка включена, а события не отмечены, магазин ничего не отправляет. Проверку при этом отправить можно.
Секрет подписи
Магазин подписывает каждое сообщение секретом, а партнёр по подписи проверяет, что сообщение пришло именно из магазина и по дороге не изменилось. Как проверять подпись, описано ниже.
Секрет выглядит так: whsec_BY28PQcMYOLVTPAuI7l1GTNbPdeWnMQMUYxQaH3BLKw=.
Магазин заводит его сам, вписывать адрес для этого не нужно. В форме секрет
скрыт точками.
- «Показать» показывает секрет целиком, «Скрыть» прячет его снова. В отличие от ключа API, секрет можно посмотреть в любой момент.
- «Скопировать» копирует секрет.
- «Выдать новый секрет» заменяет секрет. Магазин спросит «Выдать новый секрет?» и предупредит: «Прежний секрет будет действовать ещё сутки, чтобы партнёр успел заменить его у себя. После этого сообщения будут подписаны только новым секретом.» Пока прежний секрет действует, под секретом написано «Прежний секрет тоже действует до …».
Эти сутки магазин ставит на каждое сообщение две подписи, новым секретом и прежним, поэтому сайт партнёра принимает сообщения и до, и после замены секрета у себя. Выдайте новый секрет, если прежний мог попасть к посторонним.
Проверка
Кнопка «Отправить проверку» сразу отправляет на адрес из поля сообщение
webhook.test, подписанное действующим секретом, а в сутки после выдачи нового
секрета ещё и прежним. Адрес можно не сохранять
заранее. Проверка уходит, даже если отправка выключена или её выключил магазин,
и на состояние отправки не влияет.
Рядом с кнопкой появится итог:
| Итог | Что значит |
|---|---|
| «Партнёр принял проверку. Его сайт ответил за 184 мс.» | сайт ответил кодом 2xx |
| «Проверка не дошла: сайт партнёра ответил кодом 401.» | сайт ответил другим кодом. Код 401 или 403 обычно значит, что партнёр не принял подпись, то есть у него записан другой секрет |
| «Проверка не дошла. …» | сайт не ответил совсем, а после точки названа причина, например «Сайт партнёра не ответил за 10 секунд.» |
Если поле адреса пустое, под ним появится «Сначала впишите адрес.» Проверка записывается в журнал отправок, как и обычные сообщения. Повторить её кнопкой «Отправить ещё раз» нельзя, для этого нажмите «Отправить проверку» снова.
Состояние отправки
Под кнопкой проверки видно, доходят ли сообщения:
| Строка | Что значит |
|---|---|
| «Сообщения доходят до партнёра. Последнее доставлено …» | отправка включена, неудач нет. Без второй фразы, если сообщений ещё не было |
| «Сообщения не доходят с …. Магазин продолжает отправлять их повторно, но если адрес не ответит до …, отправка выключится.» | адрес перестал принимать сообщения. Первая дата — когда начались неудачи. Вторая — время, после которого первая же неудачная попытка выключит отправку. Это время будет не раньше чем через трое суток после первой даты |
| «Магазин выключил отправку …, потому что адрес трое суток не принимал сообщения.» | адрес не отвечал 72 часа, и магазин выключил отправку сам |
| «Магазин выключил отправку …, потому что сайт партнёра ответил, что сообщения больше не нужны.» | сайт партнёра ответил кодом 410 |
| «Ключ отозван, истёк или не привязан к контакту, поэтому сообщения партнёру не отправляются.» | отправка включена, но сам ключ не работает. Когда ключ снова заработает, сообщения пойдут сами |
Строки нет, пока отправка выключена или адрес не вписан.
Когда адрес не принимает сообщения 12 часов, магазин один раз предупреждает об этом: «Сообщения по ключу «…» не доходят до партнёра». В тексте предупреждения названо время, с которого адрес не принимает сообщения, и время, до которого он должен ответить. Когда магазин выключает отправку, приходит «Сообщения по ключу «…» выключены». Оба уведомления появляются в центре уведомлений панели у сотрудников с правом «Выдавать ключи API» и приходят письмом на почту для уведомлений магазина, а если её нет, то на почту магазина из основных настроек. В письме есть ссылка на ключ.
Когда партнёр наладит приём, нажмите «Включить снова» в строке состояния. Если вписать другой адрес, магазин тоже включит отправку снова и забудет прежние неудачи.
Если вы сами выключили переключатель «Отправлять сообщения», магазин отменяет сообщения, которые ещё ждали отправки, а в журнале у них написано «Отменено: отправка выключена».
После перерыва магазин сообщает, что изменилось всё. Пока сообщения не
уходили, партнёр мог пропустить что угодно. Поэтому магазин сразу отправляет
{"all": true} по ценам и по остаткам, если эти события отмечены, когда:
- вы нажали «Включить снова»;
- вы сами включили переключатель «Отправлять сообщения»;
- вы вписали другой адрес, когда отправку выключил магазин;
- ключ, который не работал, снова заработал: вы нажали «Выдать ключ снова» или продлили срок действия.
Даже если одно сохранение совпало с несколькими такими случаями, партнёр
получит по одному сообщению на каждое событие. Пропущенные сообщения о заказах
магазин не досылает, их партнёр найдёт через
GET /orders?updated_since=….
Журнал отправок
Под блоком «Сообщения партнёру» стоит блок «Журнал отправок». В нём сообщения этому партнёру за последние 30 дней, новые сверху. Старые записи магазин удаляет каждую ночь. Список подгружается кнопкой «Показать ещё».
В строке журнала:
- код ответа сайта партнёра или «—», если ответа не было;
- что за сообщение, например «Сменился статус заказа #1005120», «Изменения в заказе #1005120: состав, суммы», «Могли измениться цены по 3 артикулам» или «Изменились остатки по всем товарам». Номер заказа открывает заказ в панели;
- состояние сообщения;
- когда магазин записал сообщение и сколько было попыток.
| Состояние | Что значит |
|---|---|
| «В очереди» | сообщение ещё не отправлялось |
| «Повтор …» | попытка не удалась, следующая будет в указанное время |
| «Доставлено» | сайт партнёра ответил кодом 2xx |
| «Не доставлено» | все попытки по графику кончились неудачей, сайт ответил кодом 410, не удалась попытка, запущенная вручную, или не дошла проверка |
| «Отменено: ключу эти данные больше не видны» | у ключа нет права, событие больше не отмечено, ключ отозван или истёк либо заказ больше не принадлежит контакту ключа |
| «Отменено: отправка выключена» | отправку выключили вы или магазин, пока сообщение ждало |
| «Заменено общим сообщением по всем товарам» | адрес не отвечал, и магазин объединил накопившиеся сообщения о ценах или остатках в одно {"all": true} |
Нажмите на строку, чтобы раскрыть сообщение. Там видно его тело и
webhook-id, а под ним каждая попытка: номер, время, код ответа или причина
неудачи словами, сколько миллисекунд шёл запрос и пометка «вручную», если
попытку запустили кнопкой. Если сайт партнёра что-то ответил, видно начало
ответа, до 2 КБ.
Причины неудачи без ответа:
| Текст в журнале | Что случилось |
|---|---|
| «Сайт партнёра не ответил за 10 секунд.» | соединение есть, но ответа нет дольше 10 секунд |
| «Сайт партнёра не принял соединение за 5 секунд.» | сайт не принял соединение за 5 секунд |
| «Сайт партнёра недоступен. Возможно, адрес указан с ошибкой или сайт сейчас не работает.» | соединение не принято или оборвано без ответа |
| «У сайта партнёра неверный или просроченный сертификат https.» | ошибка шифрованного соединения |
| «Магазин не нашёл этот адрес в интернете или адрес ведёт во внутреннюю сеть, поэтому сообщение не отправлено.» | адрес не прошёл проверку перед отправкой |
| «Не удалось связаться с сайтом партнёра.» | другой сбой соединения |
У сообщения со статусом «Не доставлено» или «Отменено» есть кнопка «Отправить
ещё раз». Магазин сделает одну попытку вне графика в ближайшую минуту и
покажет «Сообщение отправится в ближайшую минуту». У повтора тот же
webhook-id, поэтому сайт партнёра узнает его, если уже обрабатывал это
сообщение. Если и эта попытка не удастся, сообщение снова станет «Не
доставлено», повторов по графику после неё не будет. Пока отправка выключена,
кнопка отвечает «Отправка сообщений этому партнёру выключена. Включите её на
этой странице и отправьте сообщение ещё раз.» Если ключу заказ к этому времени
уже не виден, сообщение снова будет отменено.
Если передать ключ другому контакту
Адрес сообщений принадлежит прежнему партнёру, поэтому после смены контакта в поле «Кому выдаёте ключ» магазин:
- выключает отправку;
- отменяет сообщения, которые ждали отправки;
- выдаёт новый секрет подписи. Прежний перестаёт действовать сразу, без суток запаса.
Адрес и отмеченные события остаются в форме. После сохранения магазин покажет «Вы передали ключ другому контакту, поэтому магазин выключил отправку сообщений и выдал новый секрет подписи. Проверьте адрес, передайте новый секрет партнёру и включите отправку снова.» Если отправка и так была выключена, сообщение короче: «Вы передали ключ другому контакту, поэтому магазин выдал новый секрет подписи. Передайте его партнёру, прежде чем включить отправку.»
Сам ключ API при смене контакта тоже меняется. Это описано на странице «Ключи доступа и права».
Сообщение
Дальше страница написана для разработчика партнёра.
Магазин отправляет запрос POST на адрес партнёра. Тело запроса — JSON в
кодировке UTF-8.
POST /cartix/webhook HTTP/1.1
Host: partner.example.ua
Content-Type: application/json
User-Agent: CartiX-Webhooks/2.120.0
webhook-id: msg_01k7xq3m9d2v8w6r4t0y5z1abc
webhook-timestamp: 1792402202
webhook-signature: v1,3SzMRkXefMzwvzMCD1IbStsAfVLji5r4riITbB/82TU=
{"type":"order.updated","timestamp":"2026-10-19T09:30:02Z","api_version":"1","data":{"order_id":5120,"changed":["items","totals"]}}
Заголовки
Формат заголовков и подписи взят из открытой спецификации Standard Webhooks (standardwebhooks.com), поэтому подпись можно проверять и готовой библиотекой Standard Webhooks.
| Заголовок | Что значит |
|---|---|
webhook-id |
номер сообщения: msg_ и 26 латинских букв и цифр в нижнем регистре. Он одинаковый во всех попытках доставить одно сообщение и при повторе вручную из журнала. По нему отбрасывайте повторы |
webhook-timestamp |
время этой попытки в секундах Unix. У каждой попытки своё |
webhook-signature |
подпись: v1, и подпись в base64. Сутки после выдачи нового секрета здесь две подписи через пробел, сначала новым секретом, потом прежним |
Content-Type |
всегда application/json |
User-Agent |
CartiX-Webhooks/ и версия магазина |
Тело
{
"type": "order.updated",
"timestamp": "2026-10-19T09:30:02Z",
"api_version": "1",
"data": {
"order_id": 5120,
"changed": ["items", "totals"]
}
}
Здесь тело показано с отступами для удобства чтения. На самом деле магазин присылает его одной строкой, без пробелов, а косую черту и кириллицу не экранирует. Подпись проверяйте по телу ровно в том виде, в каком оно пришло.
| Поле | Тип | Что значит |
|---|---|---|
type |
строка | название события, все события описаны ниже |
timestamp |
строка | когда магазин записал сообщение, в UTC. Одинаково во всех попытках, в отличие от заголовка webhook-timestamp |
api_version |
строка | версия формата сообщений, сейчас "1" |
data |
объект | номера, у каждого события свои. У проверки пустой объект {} |
Магазин записывает тело один раз и при повторах отправляет его без изменений, байт в байт.
События
| Событие | Когда приходит | data |
Что сделать |
|---|---|---|---|
order.status_changed |
сменился статус заказа | order_id |
GET /orders/{id} |
order.updated |
менеджер изменил заказ, сменилась оплата или номер накладной | order_id, changed |
GET /orders/{id} |
prices.changed |
цены артикулов могли измениться | sku_ids или all |
GET /prices?sku_ids=… или все цены |
inventory.changed |
изменились остатки артикулов | sku_ids или all |
GET /inventory?sku_ids=… или все остатки |
webhook.test |
владелец нажал «Отправить проверку» | пустой объект | ответить 2xx |
О новом заказе магазин не сообщает. Заказ через API партнёр создаёт сам, а
заказы, которые для его контакта оформил менеджер, и заказы, которые партнёр
сам оформил на витрине, найдутся через
GET /orders?created_since=….
order.status_changed
{"type":"order.status_changed","timestamp":"2026-10-19T09:30:02Z","api_version":"1","data":{"order_id":5120}}
Сменился статус заказа. В order_id внутренний номер заказа, это поле id
заказа в API, а не номер вида #1005120, который видят в панели. Новый статус в сообщении не приходит, его отдаёт
GET /orders/{id} в поле
status.
Сообщение приходит по всем заказам контакта, которому выдан ключ, откуда бы
они ни пришли: переданным через API, оформленным менеджером, купленным на
витрине или пришедшим с площадки. Те же заказы ключ видит в
GET /orders. Если у контакта
несколько ключей с этим событием, сообщение получит каждый.
Статус меняет менеджер, дополнение перевозчика или оплата, и сообщение приходит в любом из
этих случаев. Бывает, что одно действие даёт два сообщения. Например, статус,
который сам отмечает заказ оплаченным, пришлёт order.status_changed и
order.updated с "changed": ["payment"].
order.updated
{"type":"order.updated","timestamp":"2026-10-19T09:30:02Z","api_version":"1","data":{"order_id":5120,"changed":["shipping","payment"]}}
Заказ изменился. В форме ключа флажок этого события называется «Изменения в
заказе». В changed перечислено, что изменилось. Названия групп совпадают с
блоками ответа GET /orders/{id},
поэтому сразу видно, где искать правку. Самих значений в сообщении нет.
| Группа | Что изменилось | Где смотреть в заказе |
|---|---|---|
items |
менеджер добавил или убрал позицию, изменил количество или цену позиции | items |
totals |
менеджер изменил скидку заказа, стоимость доставки или итог. Сюда же попадает доплата или частичный возврат денег, если состояние оплаты при этом не сменилось | totals |
customer |
имя, телефон, почта или компания заказчика | customer |
recipient |
любое поле получателя: имя, телефон, компания и другие поля формы получателя | recipient |
shipping |
способ доставки, адрес доставки (страна, область, индекс, город, улица, дом, квартира) или отделение либо почтомат | shipping |
payment |
способ оплаты или состояние оплаты, например заказ оплатили или деньги вернули | payment, payment_status |
waybill |
появился или сменился номер накладной перевозчика. Его пишет менеджер, дополнение перевозчика или площадка | shipping.waybill_number |
comment |
комментарий к заказу | comment |
Группы приходят в том порядке, как они перечислены в таблице. Правка в
панели, которая затронула состав, обычно меняет и суммы, тогда придёт
["items", "totals"]. Номер накладной, оплата и правка в карточке заказа
приходят отдельными сообщениями, даже если случились почти одновременно.
Если менеджер нажал «Сохранить» в заказе, ничего не поменяв, сообщения не
будет. Смена статуса приходит своим событием order.status_changed, а не
order.updated.
prices.changed
{"type":"prices.changed","timestamp":"2026-10-19T09:31:00Z","api_version":"1","data":{"sku_ids":[2108,2109,2110]}}
{"type":"prices.changed","timestamp":"2026-10-19T09:31:00Z","api_version":"1","data":{"all":true}}
Цены этих артикулов могли измениться. В sku_ids номера артикулов по
возрастанию, не больше 100 в одном сообщении. Если за минуту изменилось
больше 100 артикулов, придёт несколько сообщений. Если больше 1000, придёт одно
сообщение {"all": true}, и тогда заберите цены целиком. В сообщение
попадают артикулы опубликованных товаров, то есть те, что видны через API, и
артикулы, которые из продажи пропали.
Магазин сообщает, что цена могла измениться, и не пересчитывает её для
вашего контакта. Поэтому бывают ложные срабатывания: цена для вас осталась
прежней, а сообщение пришло. Например, владелец поменял обычную цену артикула,
а вы покупаете по своей персональной цене. Сверяйте цену из
/prices с той, что записана у вас.
Сообщение приходит, когда:
- владелец или импорт изменил цену, старую цену или валюту цены артикула;
- магазин пересчитал цену со скидкой, например началась или кончилась акция каталога;
- изменились упаковки товара, шаг продажи, наименьшее количество или единица
продажи, потому что от них зависят ступени цены
tiers; - дополнение цен, например «Оптовые цены», сообщило магазину о своей правке;
- сменился курс валюты, в которой заданы цены артикулов. Придут номера артикулов с ценой в этой валюте;
- в магазине появился новый артикул или товар опубликовали, впервые или снова;
- артикул пропал из продажи, об этом ниже.
Сообщение {"all": true} приходит, когда:
- сменилась основная валюта магазина, курс валюты какой-нибудь витрины или валюта витрины, потому что тогда меняются цены всего каталога;
- дополнение цен сообщило, что цены изменились у всего каталога. «Оптовые цены» так делают, например, после правки скидки категории покупателей или своих настроек;
- ваш контакт перевели в другую категорию покупателей, удалили его категорию или объединили его карточку с другой. Такое сообщение получают только ключи этого контакта, и сразу, без минутного накопления;
- отправка возобновилась после перерыва: владелец нажал «Включить снова», сам включил отправку, вписал новый адрес или снова выдал ключ. Все случаи перечислены выше;
- адрес не отвечал, и магазин объединил ждущие сообщения в одно, об этом ниже.
Номера sku_ids можно передать в
GET /prices?sku_ids=… как есть. Этот
адрес принимает до 100 номеров за раз. Получив {"all": true}, перечитайте
/prices целиком, листая по after_id.
inventory.changed
{"type":"inventory.changed","timestamp":"2026-10-19T09:31:00Z","api_version":"1","data":{"sku_ids":[2108,2115]}}
Остатки этих артикулов изменились. Номера устроены так же, как у
prices.changed. Они идут по возрастанию, в одном сообщении их не больше 100,
а если за минуту изменилось больше 1000 артикулов, придёт одно
{"all": true}.
Магазин сообщает только об остатках, которые видит ваш ключ. Если владелец выбрал ключу склады в поле «С каких складов», сообщение придёт только о движении на этих складах. Если склады не выбраны, то о движении на складах, с которых продаёт хотя бы одна витрина. Как выбираются склады, описано на странице «Остатки».
Сообщение приходит, когда:
- на складе ключа изменилось, сколько лежит или сколько отложено: приход, продажа, резерв под заказ, отмена резерва, перемещение;
- владелец снял артикул с продажи или вернул в продажу;
- владелец изменил общий остаток артикула, у которого остаток не разложен по складам, или перестал вести остаток, или начал его вести;
- товар опубликовали, впервые или снова;
- артикул пропал из продажи, об этом ниже.
Сообщение {"all": true} приходит, когда:
- владелец переименовал, переставил, включил, выключил или удалил склад, сделал его объединённым или поменял его витрины. Такое сообщение получает ключ, у которого этот склад выбран в поле «С каких складов». Ключ без выбранных складов получает его при правке любого склада, потому что витрина могла начать или перестать продавать с этого склада;
- владелец переключил «Разрешить заказ, когда товара нет», от которого зависит
preorder; - владелец изменил у вашего ключа склады или настройку «Как отдавать остатки». Его получает только ваш ключ, и приходит оно сразу;
- отправка возобновилась после перерыва, как у
prices.changed; - адрес не отвечал, и магазин объединил ждущие сообщения в одно.
Номера sku_ids можно передать в
GET /inventory?sku_ids=… как
есть. Этот адрес, как и /prices, принимает до 100 номеров за раз и отдаёт
их одной страницей. Получив {"all": true}, перечитайте /inventory целиком,
листая по after_id.
Если артикула нет в ответе
Когда опубликованный товар снимают с публикации или удаляют, а также когда
удаляют артикул опубликованного товара, магазин присылает номера его артикулов
и в prices.changed, и в inventory.changed. В /prices и /inventory
таких артикулов уже нет, потому что оба адреса молча пропускают номера,
которых не видят. Если номера из сообщения нет в ответе, артикул снят с
продажи или удалён, и его нужно убрать у себя. Что произошло с товаром, показывает
/products/ids.
О товарах, которые не были опубликованы, например о черновиках, магазин не сообщает: партнёр их не видел.
webhook.test
{"type":"webhook.test","timestamp":"2026-10-19T09:32:11Z","api_version":"1","data":{}}
Проверка, которую владелец отправил кнопкой «Отправить проверку». Проверьте подпись и ответьте 2xx. Больше ничего делать не нужно.
Проверка подписи
Подпись — HMAC-SHA256 от строки {webhook-id}.{webhook-timestamp}.{тело}.
Ключом служит секрет без приставки whsec_, раскодированный из base64. Подпись
кодируется в base64 и приходит в заголовке webhook-signature после v1,.
Чтобы проверить сообщение:
- Возьмите тело запроса ровно в том виде, в каком оно пришло, до разбора JSON. Если разобрать JSON и собрать его заново, подпись не сойдётся.
- Проверьте, что
webhook-timestampотличается от вашего текущего времени не больше чем на 5 минут в любую сторону. Так перехваченное сообщение нельзя будет прислать вам ещё раз позже. Часы сервера должны идти точно. - Посчитайте подпись своим секретом.
- Разбейте заголовок
webhook-signatureпо пробелам. Сообщение подлинное, если ваша подпись совпала хотя бы с одной из частей, начинающихся сv1,. Сравнивайте функцией с постоянным временем сравнения (hash_equalsв PHP,crypto.timingSafeEqualв Node.js), а не обычным==.
Если подпись не сошлась, ответьте 401 или 400. Магазин посчитает попытку неудачной и повторит её по графику, а владелец увидит код в журнале. Не отвечайте на неверную подпись кодом 410: он выключает отправку совсем.
PHP
<?php
$secret = getenv('CARTIX_WEBHOOK_SECRET'); // whsec_…
$body = file_get_contents('php://input');
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$header = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
// Время попытки — не дальше 5 минут от наших часов
if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit;
}
$key = base64_decode(substr($secret, strlen('whsec_')));
$expected = base64_encode(hash_hmac('sha256', $id.'.'.$timestamp.'.'.$body, $key, true));
$valid = false;
// Сутки после смены секрета подписей две, через пробел
foreach (explode(' ', $header) as $part) {
[$version, $signature] = array_pad(explode(',', $part, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $signature)) {
$valid = true;
break;
}
}
if (! $valid) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// Повтор того же сообщения — уже обработали, просто подтверждаем
if (already_processed($id)) {
http_response_code(204);
exit;
}
// Ставим в свою очередь и сразу отвечаем, данные забираем через API позже
enqueue($id, $event['type'], $event['data']);
http_response_code(204);
Функции already_processed() и enqueue() в примере ваши: первая ищет
webhook-id среди обработанных, вторая ставит задачу в вашу очередь.
Node.js
Пример для Node.js 18 и новее без сторонних пакетов. Он слушает порт 3000, а
перед ним стоит веб-сервер, который принимает https на порту 443 и передаёт
запросы сюда.
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
const secret = process.env.CARTIX_WEBHOOK_SECRET; // whsec_…
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
function verify(headers, body) {
const id = headers['webhook-id'] ?? '';
const timestamp = headers['webhook-timestamp'] ?? '';
const header = headers['webhook-signature'] ?? '';
// Время попытки — не дальше 5 минут от наших часов
if (! /^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false;
}
const expected = createHmac('sha256', key)
.update(`${id}.${timestamp}.`)
.update(body) // тело как пришло, байтами
.digest();
// Сутки после смены секрета подписей две, через пробел
return header.split(' ').some((part) => {
const [version, signature] = part.split(',');
if (version !== 'v1' || ! signature) return false;
const given = Buffer.from(signature, 'base64');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const body = Buffer.concat(chunks);
if (! verify(req.headers, body)) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(body.toString('utf8'));
// Ставим в свою очередь по req.headers['webhook-id'] и сразу отвечаем
res.writeHead(204).end();
});
}).listen(3000);
Проверочный пример
Проверить свой код можно на примере из спецификации Standard Webhooks. При
таких данных подпись должна получиться
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=.
| Что | Значение |
|---|---|
| секрет | whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw |
webhook-id |
msg_p5jXN8AQM9LWM0D4loKWxJek |
webhook-timestamp |
1614265330 |
| тело | {"test": 2432232314} |
Время в этом примере давно прошло, поэтому для проверки отключите сравнение
времени. Подпись в примере выше с секретом
whsec_BY28PQcMYOLVTPAuI7l1GTNbPdeWnMQMUYxQaH3BLKw= тоже настоящая, по нему
можно сверить свой код.
Как отвечать
| Ответ сайта партнёра | Что делает магазин |
|---|---|
| 2xx в течение 10 секунд | сообщение доставлено |
| 410 | выключает отправку сразу и сообщает владельцу. Отвечайте так, только если сообщения этому магазину больше не нужны совсем |
429 или 503 с заголовком Retry-After |
ждёт указанное время, но не больше суток, и повторяет. Retry-After принимается числом секунд или датой |
| 3xx | неудача. Магазин не переходит по перенаправлениям, впишите в форму конечный адрес |
| любой другой код, в том числе 4xx и 5xx | неудача, повтор по графику |
| нет ответа за 10 секунд, соединение не установлено за 5 секунд, ошибка сертификата | неудача, повтор по графику |
Тело ответа магазину не нужно, можно отвечать 204 без тела. Начало ответа, до
2 КБ, магазин сохраняет в журнале отправок, и его видит владелец. Не пишите
туда ничего секретного, а при ошибке напишите понятную причину, чтобы владельцу
было проще разобраться.
Отвечайте сразу, а данные забирайте потом. Если в обработчике сообщения ходить в API магазина и ждать ответа, легко не уложиться в 10 секунд. Тогда магазин посчитает попытку неудачной и пришлёт сообщение ещё раз.
Ответ 429 или 503 магазин тоже считает неудачей. Такая попытка засчитывается в график из 10 попыток, а отсчёт до выключения отправки начинается с первой такой неудачи.
Повторы и выключение отправки
Каждое сообщение магазин пытается доставить до 10 раз. Если попытка не удалась, следующая будет через:
| После попытки | Следующая через |
|---|---|
| 1 | 5 секунд |
| 2 | 5 минут |
| 3 | 30 минут |
| 4 | 2 часа |
| 5 | 5 часов |
| 6 | 10 часов |
| 7 | 14 часов |
| 8 | 20 часов |
| 9 | 24 часа |
К каждой паузе, кроме последней, магазин добавляет или отнимает до 10 %, чтобы сообщения на
разные адреса не уходили в одну секунду. Повторы магазин запускает раз в
минуту, поэтому короткие паузы на деле бывают до минуты длиннее. Если сайт
ответил 429 или 503 с Retry-After, вместо паузы из таблицы магазин ждёт
указанное время. Если не удалась и десятая попытка, сообщение получает статус
«Не доставлено», и больше магазин его не отправляет. Весь график занимает
чуть больше трёх суток.
Перед каждой попыткой магазин заново проверяет, что ключ работает, право у него есть, событие отмечено, отправка включена, а заказ из сообщения по-прежнему принадлежит контакту ключа. Если нет, сообщение отменяется.
Если сайт не ответил на сообщение, остальные сообщения на этот адрес магазин отложит на минуту, до следующей отправки, а не будет отправлять их подряд.
Сообщения о ценах и остатках объединяются. Пока адрес не отвечает, они не
копятся. Если сообщение о ценах уже ждёт повтора, а приходит ещё одно, магазин
отменяет все ждущие сообщения о ценах и записывает вместо них одно
{"all": true}. С остатками так же. В журнале у отменённых написано «Заменено
общим сообщением по всем товарам». Когда сайт заработает, вы получите одно
сообщение и сверите цены или остатки целиком. Сообщения о заказах не
объединяются, у каждого свой график.
Предупреждение и выключение. Магазин отсчитывает время от первой неудачи после последней удачной доставки. Любой ответ 2xx обнуляет отсчёт. Отсчёт начинается заново и в двух других случаях:
- между двумя неудачами прошло больше 30 часов. Поэтому одна случайная ошибка через неделю после прошлой отправку не выключит;
- после неудачной попытки у адреса не осталось ни одного сообщения, которое ждёт повтора. Например, последнее сообщение получило статус «Не доставлено».
Пока отсчёт идёт, магазин поступает так:
- Через 12 часов неудач владелец один раз получает предупреждение, что сообщения не доходят и с какого времени. Оно приходит при первой неудачной попытке после 12 часов.
- Через 72 часа неудач магазин выключает отправку, отменяет все ждущие
сообщения и сообщает владельцу. Последняя попытка по графику всегда
приходится на время после 72 часов, если только сайт не попросил подождать
заголовком
Retry-After, поэтому она и решает, выключать ли отправку. - Ответ 410 выключает отправку сразу, без ожидания.
Включить отправку снова может только владелец, ваш сайт этого не может. Владелец нажимает «Включить снова» в форме ключа или вписывает другой адрес.
Порядок и повторы сообщений
Порядок не гарантирован. Повтор старого сообщения может прийти позже
нового, а два сообщения об одном заказе могут прийти не в том порядке, в
котором случились события. Не стройте логику на порядке сообщений. Получив
сообщение о заказе, запросите
GET /orders/{id} и сравните
поле updated_at с тем, что записано у вас. Если у вас запись того же времени
или новее, ничего делать не нужно. Цены и остатки после сообщения просто
читайте заново, API всегда отдаёт текущие.
Одно сообщение может прийти несколько раз. Так бывает, если ваш сайт
обработал сообщение, но ответил позже 10 секунд, или если владелец отправил его
ещё раз из журнала. У всех попыток одного сообщения одинаковый webhook-id.
Запоминайте номера обработанных сообщений и отвечайте 2xx на повтор, не
обрабатывая его второй раз. Храните номера хотя бы 30 дней, столько живёт
журнал, из которого владелец может отправить сообщение ещё раз.
Сообщение может не прийти совсем. Если адрес долго не отвечал, магазин
выключит отправку, а пропущенные сообщения о заказах не дошлёт. Поэтому
регулярная сверка через updated_since остаётся нужна, только делать её можно
реже.
Чего нет
- Настройки сообщений через API. Адрес, события и секрет задаёт только владелец в форме ключа.
- Сообщения о новом заказе.
- Данных в сообщении: только номера.
- Сообщений о покупателях.
- Нескольких адресов у одного ключа.
- Адресов
http://, адресов со своим портом и адресов внутренней сети. - Перехода по перенаправлениям.