Sign in
Введение/Уведомления Webhook

Webhook-уведомления

Получайте обновления статусов платежей и выплат в реальном времени через webhook'и с подписью HMAC.

Система 2328.io отправляет webhook на ваш url_callback каждый раз, когда меняется статус платежа. Это рекомендованный способ узнавать об успешных оплатах.

Формат запроса

  • Метод: POST
  • Content-Type: application/json
  • Подпись: поле sign в теле запроса

Payload

Тело webhook'а повторяет формат ответа /v1/payment/info и дополнительно содержит tx_explorer_url и поле sign для проверки подписи.

Успешный платёж

JSON
{
  "uuid": "db17d490-15b6-47b9-9015-91d1d8b119f2",
  "order_id": "ORDER-12345",
  "amount": "180.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/db17d490-15b6-47b9-9015-91d1d8b119f2",
  "expires_at": "2026-05-09T16:56:58+03:00",
  "created_at": "2026-05-09T15:56:58+03:00",
  "payer_currency": "TON",
  "payer_amount": "0.95256917",
  "network": "TON",
  "address": "UQA0RevhkCQx-EltyNgPPeG8dqtnCz7ZslOzMdNQlLxVaNBb",
  "payment_status": "paid",
  "txid": "41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "tx_explorer_url": "https://tonviewer.com/transaction/41c2a327323480af8e705d05deb09c238a41779928832abef4bb77c862357b11",
  "payment_amount": "0.95256917",
  "merchant_amount": "0.949711462490000000",
  "amount_usd": "2.41324380",
  "exchange_rate": "0.01340691",
  "sign": "6f8c15b6e53b506d5bfa38ed3fb3b50697af73434262153c02e412541372f04d"
}

Отменённый или неуспешный платёж

Если платёж не находится в финальном состоянии paid, поля txid, payment_amount и merchant_amount равны null:

JSON
{
  "uuid": "48edaf2d-2c49-4638-8f86-88636f661c1f",
  "order_id": "ORDER-12345",
  "amount": "2800.00000000",
  "currency": "RUB",
  "url": "https://go.2328.io/48edaf2d-2c49-4638-8f86-88636f661c1f",
  "expires_at": "2026-05-09T06:19:04+03:00",
  "created_at": "2026-05-09T05:19:04+03:00",
  "payer_currency": "ETH",
  "payer_amount": "0.01620968",
  "network": "ETH-ERC20",
  "address": "0x37c20d6d96d130Bc5B33D832e43b8e16aACe0c59",
  "payment_status": "cancel",
  "txid": null,
  "tx_explorer_url": null,
  "payment_amount": null,
  "merchant_amount": null,
  "amount_usd": "37.53934800",
  "exchange_rate": "0.01340691",
  "sign": "40ce68ad9691ad54e684329d75ab5adaf5b01409a2d18d3e0110b8c1be605342"
}

Описание полей

ПолеТипОписание
uuidstringUUID платежа
order_idstringВаш ID заказа
amountdecimal (8 знаков)Сумма в фиате currency
currencystringФиатная валюта, в которой мерчант создал счёт
urlstringURL хостед-чекаута
expires_atstring (ISO 8601)Когда истекает платёжная сессия
created_atstring (ISO 8601)Когда была создана платёжная сессия
payer_currencystringКриптовалюта, которой платит плательщик
payer_amountdecimal (8 знаков)Ожидаемая сумма в крипте
networkstringБлокчейн-сеть
addressstringАдрес депозита
payment_statusstringОдно из: pending, check, paid, underpaid_check, underpaid, overpaid, cancel, aml_lock (см. References)
txidstring | nullХеш транзакции в блокчейне, появляется только после подтверждения оплаты
tx_explorer_urlstring | nullСсылка на транзакцию в блокчейн-эксплорере. null, если txid отсутствует или перевод выполнен как внутренний P2P.
payment_amountdecimal | nullФактически оплаченная сумма, появляется только после оплаты
merchant_amountdecimal (18 знаков) | nullСумма, зачисленная мерчанту после комиссий
amount_usddecimal (8 знаков)Сумма в USD на момент создания
exchange_ratedecimalИспользованный курс крипто / фиат
signstring (hex)Подпись HMAC-SHA256 payload'а

Проверка подписи

Чтобы проверить подпись webhook'а:

  1. Извлеките поле sign из payload'а
  2. Удалите поле sign из объекта
  3. Закодируйте оставшиеся поля как JSON
  4. Закодируйте JSON в Base64
  5. Рассчитайте HMAC-SHA256 от строки Base64 с использованием API_KEY
  6. Сравните полученную подпись со значением sign функцией постоянного времени
PHP
<?php
function verifyWebhookSign(array $data, string $apiKey): bool {
    $receivedSign = $data['sign'] ?? '';
    unset($data['sign']);

    $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $base64 = base64_encode($json);
    $calculated = hash_hmac('sha256', $base64, $apiKey);

    return hash_equals($calculated, $receivedSign);
}

$apiKey = 'YOUR_API_KEY';
$payload = json_decode(file_get_contents('php://input'), true);

if (!verifyWebhookSign($payload, $apiKey)) {
    http_response_code(401);
    exit;
}

switch ($payload['payment_status']) {
    case 'paid':
    case 'overpaid':
        // Credit the order — check idempotency by order_id first
        break;
    case 'underpaid_check':
    case 'underpaid':
    case 'cancel':
        break;
}

http_response_code(200);

Всегда проверяйте подпись перед зачислением средств пользователю. Webhook без подписи или с неверной подписью может быть подделкой.

Webhook'и выплат

При смене status выплаты система отправляет POST webhook на URL url_callback, переданный при создании выплаты. Если url_callback не был указан, webhook'и для этой выплаты не отправляются.

Webhook'и выплат должны проверяться Payout API key, а не обычным API key. Алгоритм подписи идентичен webhook'ам платежей (удалить sign, JSON-кодирование, base64, HMAC-SHA256), отличается только ключ.

Payload

JSON
{
  "uuid": "019dff1f-0dbd-7277-8d45-271e7775388f",
  "order_id": "4dfdcc84402b1185b71cbe399321533e",
  "status": "completed",
  "currency": "TRX",
  "network": "TRX-TRC20",
  "amount": "3.00",
  "merchant_amount": "3.00",
  "network_amount": "3.00",
  "amount_usd": "1.04",
  "to_address": "THauRv5tcucQRohXg8NiyGTk16DX1XQG5x",
  "memo": null,
  "txid": "9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "tx_explorer_url": "https://tronscan.org/#/transaction/9242e533703704ef3eaba840f70b4a26333e72c943377ee375fea17badb53def",
  "block_number": null,
  "error_type": null,
  "created_at": "2026-05-07T00:08:38+03:00",
  "updated_at": "2026-05-07T00:08:54+03:00",
  "from_currency": "USDT",
  "debited_amount": "1.050735",
  "debited_currency": "USDT",
  "sign": "925ad7bf3d6841864101f7cc2c7e30652e70a06cdb04dbe07a0129480000ce4a"
}

Описание полей

ПолеТипОписание
uuidstringUUID выплаты
order_idstringВаш ID идемпотентности / референс, если был передан
statusstringpending, completed, failed, cancelled (см. References)
currencystringВалюта вывода
networkstringБлокчейн-сеть
amountdecimalСумма вывода (в currency)
merchant_amountdecimalСумма, списанная с баланса мерчанта
network_amountdecimalСумма, фактически отправленная в сеть
amount_usddecimalСтоимость в USD на момент выплаты
to_addressstringБлокчейн-адрес получателя
memostring | nullMemo / destination tag, если использовался
txidstring | nullХеш транзакции в блокчейне, заполняется при completed
tx_explorer_urlstring | nullСсылка на транзакцию в блокчейн-эксплорере. null, если txid отсутствует или перевод выполнен как внутренний P2P.
block_numberinteger | nullВысота блока on-chain транзакции
error_typestring | nullПричина при status = failed (например, aml_risk, см. References)
created_atstring (ISO 8601)Когда была создана выплата
updated_atstring (ISO 8601)Когда статус последний раз менялся
from_currencystringИсходный баланс, с которого было списание при авто-конвертации (например, USDT для выплаты в BTC)
debited_amountdecimalСумма, списанная с баланса from_currency
debited_currencystringВалюта списания
signstring (hex)Подпись HMAC-SHA256 payload'а, рассчитанная Payout API key

Лучшие практики

  • Идемпотентность — всегда проверяйте, не был ли платёж уже обработан (по order_id или uuid). Webhook'и могут приходить несколько раз.
  • Быстрый ответ — возвращайте HTTP 200 как можно быстрее. Тяжёлую работу выносите в фоновую очередь.
  • Повторы — если система не получит HTTP 200, webhook отправляется повторно через 2 минуты. Максимум 5 попыток повтора.
  • Асинхронная обработка — обрабатывайте события webhook'ов асинхронно, чтобы не блокировать ответ.
  • Безопасность — ВСЕГДА проверяйте подпись sign, прежде чем доверять payload'у.

Webhook'и могут приходить не по порядку. Не считайте, что первый полученный webhook отражает финальное состояние — при необходимости перепроверяйте через /v1/payment/info (или /v1/payout/status/{uuid}).

Правила доставки и обработки

Обрабатывайте запросы на webhook-эндпоинте в следующем порядке:

  1. Прочитайте тело запроса, не записывая в логи секреты и полное значение подписи.
  2. Определите тип события: платёж или статический кошелёк либо выплата. От этого зависит выбор правильного API-ключа.
  3. Удалите sign, воспроизведите документированное JSON-представление, вычислите HMAC-SHA256 и сравните подписи за постоянное время.
  4. Проверьте обязательные идентификаторы, десятичные строки и значения статуса.
  5. Атомарно создайте запись входящего события с ключом идемпотентности. Если запись уже существует, верните HTTP 200 без повторного выполнения побочных эффектов.
  6. В одной транзакции зафиксируйте изменение заказа или реестра, а некритичные письма, аналитику и уведомления поставьте в очередь.
  7. Быстро верните HTTP 200.

Не вызывайте медленные внешние сервисы внутри транзакции идемпотентной обработки. Тайм-аут после фиксации изменений, но до отправки ответа, может вызвать повторную доставку; дубликат должен обнаружить сохранённый ключ входящего события и ничего не менять.

СобытиеОсновной идентификаторПримечание
Платёжная сессияuuid + статус или версияУ одного счёта может быть несколько корректных изменений статуса.
Частичная оплатаuuid счёта + txidК одному недоплаченному счёту могут относиться несколько переводов.
Депозит статического кошелькасеть + txidОдин order_id повторно используется для каждого пополнения этого кошелька.
Выплатаuuid выплаты + статусЛогика повторной обработки webhook никогда не должна создавать вторую выплату.

Если схема не содержит отдельного идентификатора события, сохраните хеш проверенного тела запроса как дополнительное аудиторское доказательство. Не заменяйте перечисленные бизнес-идентификаторы временной меткой.

Порядок событий и сверка

Доставка выполняется как минимум один раз, а уведомления о статусах могут прийти не по порядку. Используйте монотонные бизнес-правила, а не подход «последний запрос побеждает»:

  • не возвращайте исполненный заказ в check только потому, что старое событие пришло с опозданием;
  • разрешайте underpaid_check принимать дополнительные txid, не повторяя прежние зачисления;
  • считайте paid и overpaid успешными финансовыми состояниями, но сохраняйте различия в фактических суммах;
  • сохраняйте underpaid как окончательный результат частичной оплаты, если авторитетный API позже не сообщил другое состояние;
  • отправляйте aml_lock на проверку и не позволяйте обычному обработчику повторов автоматически исполнить заказ;
  • запрашивайте актуальные данные платежа или выплаты, если переход невозможен, не хватает контекста либо финансовый результат неоднозначен.

Выполняйте периодическую сверку, даже если доставка webhook работает без видимых сбоев. Сравнивайте конечный локальный статус и зачисленную сумму с /v1/payment/info, /v1/static-wallet/transactions или /v1/payout/status/{uuid}. При расхождении создавайте предупреждение, а не перезаписывайте историю финансового реестра молча.

Безопасность webhook-эндпоинта

  • Требуйте HTTPS и оставляйте callback доступным из интернета; адреса из приватных диапазонов и loopback отклоняются при создании платежа.
  • Ограничьте размер тела запроса и принимайте JSON.
  • Применяйте rate limit до ресурсоёмкой обработки, но оставляйте запас для легитимных всплесков и повторных доставок.
  • Не авторизуйте webhook только по IP-адресу источника. Сетевой allowlist — дополнительный уровень защиты; проверка HMAC обязательна.
  • Удаляйте из логов sign, API-ключи, адреса, если этого требует политика, и персональные метаданные.
  • Во время контролируемой ротации поддерживайте текущий и явно назначенный следующий ключ; не пытайтесь угадывать, каким ключом подписано событие.
  • При неверной подписи возвращайте общее сообщение об ошибке, чтобы эндпоинт не раскрывал сведения о ключе или аккаунте.