Уведомление по умолчанию
Если в настройках Магазина была активирована опция отправки автоматических уведомлений о смене статуса Заказа, то при поступлении оплаты в пользу Заказа, система Инвойсбокс осуществит запрос на специальный URL, который указан в настройках Магазина.
На указанный URL будет осуществлен POST запрос с Content-Type: application/json в теле которого будет находиться объект OrderNotification.
OrderNotification
Повторяет структуру OrderResponse, основные поля:
| Свойство | Тип | Описание |
|---|---|---|
| id * | string(36) | Идентификатор заказа в системе Инвойсбокс, например: 01771534-1a57-f184-dee3-ebeb91dded75 |
| status * | string(50) enum | Статус заказа: created, hold, completed, expired, canceled. Оплаченным считается только completed — перечень и переходы на странице работы с заказом |
| merchantId * | string(36) | Идентификатор магазина, например: 01771534-1a57-f184-dee3-ebeb91dded76 |
| merchantOrderId * | string(100) | Идентификатор заказа в учётной системе магазина, например: O-12345 |
| merchantOrderIdVisible | string(100) | Номер заказа, отображаемый на платежной странице. Если не заполнено, показывается значение из merchantOrderId например 111TN22-33 |
| amount * | float | Сумма заказа, например: 19658.45 |
| customer * | Customer | Информация о заказчике |
| currencyId * | string(3) enum | Валюта заказа, например: RUB, USD,EUR, GBP |
| createdAt * | datetime | Дата создания заказа, например: 2026-12-22T00:00:00+00:00 |
Заказ считается оплаченным, если его статус равен completed. В этом случае Магазин должен сверить
сумму Заказа в запросе с суммой Заказа в своей системе учёта. Если не совпадает, в обработке запроса
нужно отказать и вернуть ошибку.
Важно
Поле id — идентификатор заказа, а не события. Один заказ проходит несколько статусов
(created → hold/completed/expired/canceled), и по одному заказу приходит несколько
уведомлений, поэтому дедупликация «по id» проглотит переходы: раннее created поглотит
последующее completed. Дедуплицируйте по заголовку X-Event-Id — идентификатору
уведомления: у всех попыток доставки одного уведомления он один, поэтому повтор вы узнаете
именно по нему. И в любом случае применяйте переход только вперёд, по паре id + status:
- Пара уже обработана — верните
success, ничего не меняя (повторная доставка). - Пара новая — примените переход, только если он движется вперёд по жизненному циклу
(
created→hold→completed;expired/canceled— терминальные). Уведомление со «старым» статусом после терминального не откатывает состояние — вернитеsuccessбез изменений. - Порядок доставки не гарантирован. Сомневаетесь в текущем состоянии — спросите сам заказ:
GET /v3/billing/api/order/order/{id}возвращает актуальный статус — он и есть истина.
Заголовки доставки
Тело уведомления — это данные заказа и ничего кроме. Сведения о самой доставке приходят заголовками:
| Заголовок | Описание |
|---|---|
X-Event-Id | Идентификатор уведомления: у всех попыток его доставки он один — по нему и дедуплицируйте |
X-Event-Type | Код события, например order_status_changed |
X-Event-Occurred-At | Когда событие произошло. Не путайте с createdAt в теле — тот про заказ |
X-Delivery-Attempt | Номер попытки, с единицы. Больше единицы — предыдущий ответ нас не устроил; у попыток одного уведомления X-Event-Id не меняется |
Важно
Подпись X-Signature считается от тела и заголовки не покрывает. Поэтому X-Event-Id — ключ
дедупликации, а не средство защиты: от повторно присланного (в том числе злоумышленником) тела
спасает не он, а правило «переходы только вперёд» — сверка статуса с текущим состоянием заказа.
Дедупликация по X-Event-Id избавляет от лишней работы, проверка перехода — от неверного
результата; нужны обе.
Перечень статусов закрытый: кроме пяти перечисленных, других не бывает. Статусов вида refunded или
partial_refund в заказе нет — возврат оформляется отдельным заказом
со своими статусами (draft, created, completed, canceled), а сам заказ после возврата остаётся
completed.
Формат ответа
В случае успешности обработки запроса веб-сервис Магазина должен вернуть объект NotificationSuccess, а в случае осознанной ошибки обработки (например, неверная сумма, заказ не найден, ошибка подписи и т.п.) - NotificationError.
Важно
И успешный, и осознанно-ошибочный ответ должны возвращаться с HTTP-кодом 200. HTTP-код 200 сам по себе не означает успех — успехом считается только связка HTTP 200 + {"status":"success"} в теле. Связка HTTP 200 + {"status":"error", "code": ...} — это тоже штатный, ожидаемый ответ, просто сигнализирующий об осознанной ошибке обработки (см. NotificationErrorCode).
Любой ответ с HTTP-кодом, отличным от 200 (а также ответ без валидного JSON в теле), система Инвойсбокс трактует не как конкретный код ошибки из тела, а как техническую недоступность веб-сервиса Магазина — то есть равносильно out_of_service, независимо от того, что написано в code. Такие запросы будут повторены ещё до 10 раз в течение суток (см. NotificationErrorCode).
Другими словами: code в теле ответа имеет смысл, только если HTTP-код равен 200. Если веб-сервис Магазина вернул, например, HTTP 400 или 401 — тело ответа не разбирается, и запрос будет повторён как при out_of_service, даже если в теле был указан другой код ошибки (например, signature_error).
NotificationSuccess
| Свойство | Тип | Описание |
|---|---|---|
| status * | string(50) | В случае успешной обработки допустимо только одно значение - success |
Пример объекта NotificationSuccess:
{
"status" : "success"
}
NotificationError
| Свойство | Тип | Описание |
|---|---|---|
| status * | string(50) enum | В случае ошибки обработки запроса допустимо только одно значение - error |
| code | string(100) enum | Код ошибки - значение из справочника NotificationErrorCode, по умолчанию out_of_service |
| message | string(500) | Детальное описание ошибки в текстовом формате |
Пример объекта NotificationError:
{
"status" : "error",
"code" : "order_wrong_amount",
"message" : "Сумма заказа не соответствует сумме оплаты"
}
NotificationErrorCode
| Код ошибки | Описание |
|---|---|
out_of_service | Техническая ошибка обработки запроса веб сервером Магазина, при получении этого кода ошибки система Инвойсбокс будет пытаться повторить этот запрос еще 10 раз в течение последующих суток. |
Если повторы исчерпаны
Десять повторов за сутки доставку не гарантируют: сервис мог лежать дольше. Поэтому уведомления нельзя делать единственным источником истины об оплате — нужна регулярная сверка:
- Раз в несколько минут выбирайте заказы, изменившиеся за последнее окно, с нахлёстом по времени:
GET /v3/filter/api/order/order?createdAt[_ge]=<водяной знак минус нахлёст>— постранично, сверяяmetaData.pageс запрошенной страницей. - Заказ со статусом
completedв API, но не оплаченный в вашей системе, — это пропущенное уведомление. Примените тот же обработчик, что и для уведомления (сверка суммы, идемпотентный переход), и заведите алерт: пропуски должны быть редкостью, а не фоном. - Обратная ситуация — оплачен у вас,
expired/canceledв API — повод остановить отгрузку и разобраться вручную: деньги по такому заказу не приходили.
Журнала доставок и ручной повторной отправки уведомлений в API сейчас нет — сверка выборкой и есть
штатный механизм восстановления.
| order_wrong_amount | Сумма заказа в Магазине не соответствует сумме заказа в уведомлении |
| order_already_paid | Заказ уже оплачен другим инструментом оплаты :warning: |
| order_not_found | Заказ не найден в учётной системе Магазина |
| shipping_unavailable | Услуга не может быть оказана или товар не может быть доставлен |
| full_refund_required | То же самое что shipping_unavailable с автоматическим формированием возврата или отмены оплаты |
| signature_error | Ошибка проверки подписи запроса |
Важно
Обратите внимание, в случае, если аналогичный запрос с тем же идентификатором (id) уже был обработан ранее
успешно в вашей системе, то в этом случае следует вернуть статус успешной обработки уведомления status = success.
В случае, если заказ в вашей системе был оплачен ранее уведомлением с другим идентификатором (id) или иным платёжным инструментом,
то следует вернуть ошибку status = error, code = order_already_paid.
Время выполнения запроса
Существует лимит ожидания ответа от веб-сервера Магазина на запросы уведомления, который составляет 20 секунд. Запросы отрабатывающие дольше этого значения считаются ошибочными.
Подпись запроса
При обработке запроса уведомления Магазину необходимо проверить его целостность.
Для этого необходимо сформировать подпись тела входящего запроса и сравнить со значением из заголовка X-Signature.
Если эти значения не совпадают, необходимо сформировать ответ NotificationError с
NotificationErrorCode signature_error и вернуть его с HTTP-кодом 200 — как и для
любого другого NotificationError (см. Формат ответа). Возврат HTTP 400/401/403
вместо 200 приведёт к тому, что Инвойсбокс не разберёт code из тела ответа и обработает запрос как
техническую ошибку out_of_service, а не как signature_error. Электронная подпись формируется путем
криптографического преобразования содержимого тела запроса с использованием ключа и алгоритма выбранных в настройках уведомлений.
Ключ свой у каждого магазина, а не общий на учётную запись: если магазинов несколько, проверяйте
подпись ключом того магазина, чей merchantId пришёл в теле уведомления.
Алгоритм и метод берутся из настроек уведомлений магазина. У магазинов, которые подключаются сейчас,
по умолчанию стоят метод hash_hmac и алгоритм sha256 — на новой интеграции считайте подпись
именно так. У магазинов, настроенных раньше, остаётся выбранный тогда алгоритм: как правило sha1.
Он продолжает работать, но при первой возможности переключитесь на sha256 — на стороне обработчика
это одна строка. Алгоритм видно и можно сменить там же, где выдаётся ключ: настройки интеграции
магазина в ЛК Инвойсбокс.
Пример проверки подписи на языке PHP
<?php
$xSignature = false;
foreach (getallheaders() as $header => $value) {
if (strtolower($header) === 'x-signature') {
$xSignature = $value;
break;
}
}
if (!$xSignature) {
// Ошибка, подпись запроса не получена.
// HTTP-код ответа здесь не задаётся явно, поэтому используется код 200 по
// умолчанию — это обязательно, иначе "Инвойсбокс" не разберёт code из тела.
header("Content-Type: application/json");
die('{"status":"error","code":"out_of_service"}');
}
$payload = file_get_contents("php://input");
$apiKey = ""; // Ключ подписи из настроек интеграции магазина
// Алгоритм — тот, что стоит в настройках уведомлений: у новых магазинов sha256,
// у настроенных раньше обычно sha1
$calcSignature = hash_hmac("sha256", $payload, $apiKey);
// hash_equals, а не !=: обычное сравнение отвечает тем быстрее, чем раньше строки
// разошлись, и по этому времени подпись можно подобрать
if (!hash_equals($calcSignature, $xSignature)) {
// Ошибка, подпись запроса неверная.
// HTTP-код ответа — 200 по умолчанию (см. выше), а не 400/401.
header("Content-Type: application/json");
die('{"status":"error","code":"signature_error"}');
}
Пример проверки подписи на языке Python
import hashlib
import hmac
from flask import Flask, request, jsonify
app = Flask(__name__)
# Проверьте правильность пути и метода в декораторе @app.route()
# - его нужно подстроить под ваше веб-приложение.
@app.route("/invoicebox_callback", methods=['POST'])
def invoicebox_callback():
x_signature = request.headers.get('x-signature')
if not x_signature:
# Ошибка, подпись запроса не получена.
# HTTP-код должен быть 200 — иначе "Инвойсбокс" не разберёт code из тела
# и посчитает ответ технической ошибкой (out_of_service).
response = jsonify({'status': 'error', 'code': 'out_of_service'})
response.headers["Content-Type"] = "application/json"
return response, 200
# Ключ подписи из настроек интеграции магазина
api_key = ""
payload = request.data
# Алгоритм — тот, что стоит в настройках уведомлений: у новых магазинов sha256,
# у настроенных раньше обычно sha1
calc_signature = hmac.new(api_key.encode(), payload, hashlib.sha256).hexdigest()
# compare_digest, а не !=: обычное сравнение выдаёт по времени, сколько символов совпало
if not hmac.compare_digest(calc_signature, x_signature):
# Ошибка, подпись запроса неверная.
# HTTP-код должен быть 200, а не 400/401 — см. раздел "Формат ответа".
response = jsonify({'status': 'error', 'code': 'signature_error'})
response.headers["Content-Type"] = "application/json"
return response, 200
# Подпись верна: здесь отмечаем заказ оплаченным в своей учётной системе —
# идемпотентно, потому что то же уведомление может прийти повторно.
# mark_order_paid(request.get_json())
return jsonify({'status': 'success'}), 200
Для удобства, смотрите также PHP SDK
Как проверить свой обработчик
Тратить деньги для проверки не нужно: на демо-магазине оплата подтверждается на платёжной странице автоматически. Достаточно довести заказ до платёжной страницы, и уведомление уйдёт так же, как в бою.
- Укажите адрес обработчика в личном кабинете: «Мои продажи» → «Мои магазины» →
«Интеграция (API)», поле «URL уведомления». Адрес должен быть публичным и работать по
HTTPS — на
localhostуведомление не придёт. - Создайте заказ своим токеном на своём магазине.
- Откройте
paymentUrlиз ответа. На демо-магазине оплата подтвердится сама. - Обработчик получит уведомление того же формата, что в бою; ответьте
200 {"status":"success"}— иначе уведомление придёт повторно.
Если своего магазина ещё нет
Посмотреть на живое уведомление можно и без него. Заказы, созданные кнопкой «Выполнить» в документации, уходят с адресом уведомлений портала: что прислал Инвойсбокс — статус, сумма, тело запроса и результат проверки подписи — видно в песочнице на быстром старте.
Ключ подписи демо-магазина — 5a3797956281681be7cbb33ffc390ea1, метод hash_hmac. Демо-магазин
настроен давно, поэтому алгоритм у него sha1: это тот же случай, что у ваших ранних магазинов.
Контрольный пример ниже дан для обоих алгоритмов, так что проверить реализацию можно и с sha256.
На странице интеграционных данных он же
называется «API ключ» — это одно значение. Не путайте его с API токеном
(Authorization: Bearer): токеном авторизуются запросы, подпись уведомлений им не считается.
Контрольный пример, которым проверяется реализация до первого настоящего уведомления. Для сырого тела (без переводов строки в конце):
{"id":"01771534-1a57-f184-dee3-ebeb91dded75","status":"completed","amount":122.00}
подпись ключом демо-магазина равна:
hash_hmac('sha1', <сырое тело>, '5a3797956281681be7cbb33ffc390ea1')
= b16d2e8169553340772769b390a05b1a648a8a87
hash_hmac('sha256', <сырое тело>, '5a3797956281681be7cbb33ffc390ea1')
= 1cc38589fdb7bfb542c06f9f4916f449e3ab8b86bc6d35ec6e7b46f80f143500
Если ваша функция даёт другое значение — проверьте, что подписывается именно сырое тело запроса
байт в байт, а не перекодированный JSON.
С ними тот же расчёт повторяется на вашей стороне: сравните свою подпись с той, что пришла в
заголовке X-Signature.
Свой обработчик всё равно нужно проверить на своём магазине — адрес уведомлений задаётся либо в
личном кабинете, либо полем notificationUrl при создании заказа.
Если обработчика ещё нет, а посмотреть на смену статуса хочется, статус заказа можно прочитать запросом: Получение заказа.
Система мониторинга и автоматическое тестирование интеграции
В системе мониторинга Инвойсбокс реализовано автоматическое тестирование работоспособности интеграции.
Для проверки корректности интеграции, система может направлять тестовое уведомление, в котором в качестве идентификатора заказа в учётной системе магазина будет
передано пустое значение, в качестве идентификатора заказа в системе Инвойсбокс будет передано значение ffffffff-ffff-ffff-ffff-ffffffffffff.
При получении такого запроса, система учёта магазина должна проверить корректность подписи запроса, сверить идентификатор магазина с настройками и вернуть ответ по результатам проверки.