Обмен данными · Руководства
Как получать уведомления через вебхуки
Как подписаться на изменения в SelSup, принять уведомление на своём сервере и проверить его подпись.
Вебхук — это HTTP-запрос от SelSup на ваш сервер при изменении данных. Например, после создания заказа ваш обработчик может передать его в WMS. Ниже описаны настройка подписки, формат уведомлений и обработка сбоев.
Разработчики серверных интеграций1
Настройка подписки
Подготовьте публичный HTTPS-адрес обработчика на своём сервере. При создании подписки передайте его в url, укажите название в name и перечислите события в массиве types. Для событий заказов можно указать organizationId, чтобы получать уведомления только по нужной организации.
CREATE_ORDER — создание заказа; ORDER — создание или изменение заказа; ORDER_STATUS — изменение статуса; STOCK — изменение доступного остатка SKU. CREATE_PRODUCT есть в списке типов подписки, но уведомления о создании товара пока не отправляются.
Если подписка использует несколько типов, уведомления приходят на один URL. Для разных обработчиков создайте отдельные подписки. В теле уведомления нет общего обязательного поля type, поэтому отдельные URL удобнее для событий с похожей структурой.
POST /api/webhook/ возвращает созданную подписку с id и signingSecret. Сохраните секрет на сервере — он нужен для проверки подписи входящих запросов. GET /api/webhook/ также возвращает секреты подписок и требует прав ADMIN или API.
curl --request POST 'https://api.selsup.ru/api/webhook/' \
--header 'Authorization: YOUR_API_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"name": "Новые заказы для WMS",
"url": "https://integration.example.com/selsup/orders",
"types": ["CREATE_ORDER"]
}'
2
Формат уведомления
SelSup отправляет POST с Content-Type application/json и кодировкой UTF-8. Сначала проверьте подпись по исходным байтам тела; после этого разбирайте JSON.
Для CREATE_ORDER и ORDER тело содержит объект order с данными заказа. Для ORDER_STATUS передаются order и newStatus. Перечень полей заказа смотрите в описании заказов; пример ниже сокращён и показывает только расположение данных.
Для STOCK приходит объект stocks с массивом элементов skuId и quantity. Это доступный остаток SKU; в данном сообщении нет отдельного warehouseId. Не трактуйте quantity как остаток конкретной ячейки или склада.
{
"order": {
"id": 505,
"externalOrderId": "SHOP-1005"
}
}
{
"stocks": [
{"skuId": 404, "quantity": 12},
{"skuId": 405, "quantity": 0}
]
}
Связанные методы API
GET/api/order/findПоля и поиск заказов
3
Проверка подписи и приём уведомления
В каждом запросе есть заголовок X-SelSup-Signature с полями t, id и v1. t — время отправки в секундах Unix, id — идентификатор попытки доставки, v1 — подпись HMAC-SHA256 в шестнадцатеричном виде.
Для проверки подписи преобразуйте signingSecret из шестнадцатеричной строки в байты. Подпишите последовательность ASCII(t), точка, ASCII(id), точка, исходные байты тела. Сравните результат с v1 способом, время выполнения которого не зависит от совпавшей части подписи.
Отклоняйте слишком старые запросы, например старше пяти минут, и запоминайте принятые id на этот срок. Время на сервере должно быть синхронизировано. При нескольких обработчиках используйте общее хранилище принятых id.
После проверки надёжно сохраните событие, затем верните любой успешный HTTP-статус 2xx. Долгие обращения к внешним системам выполняйте позже, из очереди. В текущей реализации ожидание HTTP-ответа ограничено 700 мс, поэтому обработчик должен отвечать быстро.
Изменение пробелов или порядка полей меняет подпись. Проверяйте ровно те байты, которые пришли по HTTP. API-токен и секрет подписи вебхука — разные значения.
X-SelSup-Signature: t=1720000000,id=550e8400-e29b-41d4-a716-446655440000,v1=<64 шестнадцатеричных символа>
const crypto = require('node:crypto');
const express = require('express');
const app = express();
const signingSecret = process.env.SELSUP_WEBHOOK_SECRET;
// Зарегистрируйте маршрут до express.json().
// inbox — ваше постоянное хранилище входящих уведомлений.
app.post('/selsup/orders', express.raw({type: 'application/json'}), async (req, res) => {
const header = req.get('X-SelSup-Signature') || '';
const match = /^t=(\d+),id=([0-9a-f-]{36}),v1=([0-9a-f]{64})$/.exec(header);
if (!match || !Buffer.isBuffer(req.body)) return res.sendStatus(401);
const [, timestamp, deliveryId, hex] = match;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(401);
}
const signed = Buffer.concat([
Buffer.from(`${timestamp}.${deliveryId}.`, 'ascii'), req.body
]);
const expected = crypto.createHmac('sha256', Buffer.from(signingSecret, 'hex'))
.update(signed).digest();
const actual = Buffer.from(hex, 'hex');
if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
return res.sendStatus(401);
}
try {
// insertOnce атомарно сохраняет тело и уникальный deliveryId.
// Уже сохранённый id не создаёт новую задачу.
await inbox.insertOnce(deliveryId, req.body);
return res.sendStatus(202);
} catch (error) {
return res.sendStatus(503);
}
});
4
Как обрабатывать повторы
Повторная попытка доставки получает новый id и подпись. Проверка id защищает от повторного воспроизведения одного подписанного запроса, но не заменяет проверку повторной обработки одного изменения.
Сделайте обработку повторяемой без побочных эффектов. Например, обновляйте запись заказа по его идентификатору вместо создания новой записи при каждом уведомлении. Для операций вроде списания товара сохраняйте отдельный признак уже выполненного действия.
Не рассчитывайте на порядок поступления уведомлений. Если нужно принять решение по текущему состоянию заказа, перечитайте его через API перед действием.
5
Как восстановить пропущенные изменения
Успешный HTTP-ответ означает, что обработчик принял уведомление. Он не подтверждает завершение обмена с вашей внешней системой.
SelSup не предоставляет здесь гарантированную очередь доставки или обещание повторять каждый неуспешный запрос до успеха. Если сервер был недоступен, восстанавливайте изменения через API. Не рассчитывайте, что все пропущенные вебхуки обязательно придут позже.
Для заказов сохраняйте время последней успешной сверки и запрашивайте изменения по modifiedDate с небольшим перекрытием периода. Повторно пришедшие заказы обновляйте по ID. Остатки периодически сверяйте с данными нужного склада через API.
Связанные методы API
GET/api/order/findЗапросить изменённые заказыGET/api/wms/stock/findПроверить остатки
6
Как отключить или заменить подписку
Сначала запустите новый обработчик и проверьте его на тестовом событии. Затем создайте нужную подписку и удалите старую по её id. Если какое-то время работают обе подписки, учитывайте возможное повторное получение одного изменения.
Секрет создаётся отдельно для каждой подписки. Если он стал известен посторонним, замените подписку и её секрет. Удаление подписки прекращает последующую отправку, но не удаляет уже сохранённые сообщения из вашей очереди.