К содержимому
Инвойсбокс

Уведомление по умолчанию

Если в настройках Магазина была активирована опция отправки автоматических уведомлений о смене статуса Заказа, то при поступлении оплаты в пользу Заказа, система Инвойсбокс осуществит запрос на специальный 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
merchantOrderIdVisiblestring(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 — идентификатор заказа, а не события. Один заказ проходит несколько статусов (createdhold/completed/expired/canceled), и по одному заказу приходит несколько уведомлений, поэтому дедупликация «по id» проглотит переходы: раннее created поглотит последующее completed. Дедуплицируйте по заголовку X-Event-Id — идентификатору уведомления: у всех попыток доставки одного уведомления он один, поэтому повтор вы узнаете именно по нему. И в любом случае применяйте переход только вперёд, по паре id + status:

  1. Пара уже обработана — верните success, ничего не меняя (повторная доставка).
  2. Пара новая — примените переход, только если он движется вперёд по жизненному циклу (createdholdcompleted; expired/canceled — терминальные). Уведомление со «старым» статусом после терминального не откатывает состояние — верните success без изменений.
  3. Порядок доставки не гарантирован. Сомневаетесь в текущем состоянии — спросите сам заказ: 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
codestring(100) enumКод ошибки - значение из справочника NotificationErrorCode, по умолчанию out_of_service
messagestring(500)Детальное описание ошибки в текстовом формате

Пример объекта NotificationError:

{
  "status" : "error",
  "code" : "order_wrong_amount",
  "message" : "Сумма заказа не соответствует сумме оплаты"
}

NotificationErrorCode

Код ошибкиОписание
out_of_serviceТехническая ошибка обработки запроса веб сервером Магазина, при получении этого кода ошибки система Инвойсбокс будет пытаться повторить этот запрос еще 10 раз в течение последующих суток.

Если повторы исчерпаны

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

  1. Раз в несколько минут выбирайте заказы, изменившиеся за последнее окно, с нахлёстом по времени: GET /v3/filter/api/order/order?createdAt[_ge]=<водяной знак минус нахлёст> — постранично, сверяя metaData.page с запрошенной страницей.
  2. Заказ со статусом completed в API, но не оплаченный в вашей системе, — это пропущенное уведомление. Примените тот же обработчик, что и для уведомления (сверка суммы, идемпотентный переход), и заведите алерт: пропуски должны быть редкостью, а не фоном.
  3. Обратная ситуация — оплачен у вас, 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

Как проверить свой обработчик

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

  1. Укажите адрес обработчика в личном кабинете: «Мои продажи» → «Мои магазины» → «Интеграция (API)», поле «URL уведомления». Адрес должен быть публичным и работать по HTTPS — на localhost уведомление не придёт.
  2. Создайте заказ своим токеном на своём магазине.
  3. Откройте paymentUrl из ответа. На демо-магазине оплата подтвердится сама.
  4. Обработчик получит уведомление того же формата, что в бою; ответьте 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.

При получении такого запроса, система учёта магазина должна проверить корректность подписи запроса, сверить идентификатор магазина с настройками и вернуть ответ по результатам проверки.

Страница помогла?