# Тело события вебхука

> Разбирайте версионированный webhook-envelope и полный снимок инвойса, вывода или депозита платёжного канала в поле data.

Тело каждого вебхука — версионированный envelope. Объект `data` совпадает с контрактом ресурса для мерчанта из соответствующего status API, поэтому обработчик применяет полный снимок, а не собирает состояние из частичных изменений. Это верно для всех трёх ресурсов: инвойсы, выводы и депозиты платёжных каналов используют один envelope и одно правило для тела события.

## Оболочка события

| Поле | Тип | Контракт |
|------|-----|----------|
| `event_id` | string | Стабильный ID с префиксом `evt_…`, совпадает с `X-Webhook-Id` и не меняется при повторах и переотправке |
| `event_type` | string | Одно из значений [каталога событий](/ru/docs/webhooks/events.md) |
| `version` | integer | Версия схемы тела события. Текущая версия — `1` |
| `occurred_at` | Unix seconds | Время перехода ресурса; в отличие от времени доставки не меняется при retry |
| `data` | object | Полный снимок инвойса, вывода или депозита платёжного канала для этого события |

`event_id` — идентификатор **доставки**: копия одного перехода для одного endpoint'а. Два endpoint'а, подписанных на одно событие, получат два разных значения `evt_` по одному и тому же платежу. **Бизнес**-идентификатор — это id ресурса внутри `data` (`inv_…`, `wdr_…` или `pcd_…`), и он одинаков для всех endpoint'ов, переходов и повторных доставок. Деньги дедуплицируйте по бизнес-идентификатору, попытки обработки — по `event_id`.

Сохраните `event_id`, `event_type`, `version` и исходное тело запроса до запуска тяжёлой обработки. Если версия схемы не поддерживается вашей интеграцией, отклоните её явно — не пытайтесь угадать структуру.

## Тело события инвойса

Пример: `invoice.paid`.

```json
{
  "event_id": "evt_J7EEYeL9pZJukfj2c5OQ44",
  "event_type": "invoice.paid",
  "version": 1,
  "occurred_at": 1739281200,
  "data": {
    "invoice_id": "inv_5CcyDYmMUGtzYL10q0Iimr",
    "project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
    "status": "paid",
    "is_final": true,
    "is_test": false,
    "payment_url": "https://checkout.paymos.io/invoice/inv_5CcyDYmMUGtzYL10q0Iimr",
    "order": {
      "external_id": "order-12345",
      "client_id": "customer-67890",
      "amount": "100.00",
      "currency": "USD"
    },
    "payment": {
      "currency": "USDT",
      "network": "TRC20",
      "chain_id": 728126428,
      "contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
      "expected": "50.00",
      "address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
      "exchange_rate": "2.00",
      "paid": "50.00",
      "remaining": "0",
      "fee": "0.50",
      "net": "49.50",
      "transfers": [
        {
          "tx_hash": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
          "amount": "50.00",
          "status": "confirmed",
          "created_at": 1739281020,
          "confirmed_at": 1739281200,
          "required_confirmations": 19,
          "estimated_confirmation_at": 1739281080,
          "explorer_url": "https://tronscan.org/#/transaction/abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
        }
      ]
    },
    "expires_at": 1739284200,
    "completed_at": 1739281200,
    "created_at": 1739277600,
    "updated_at": 1739281200
  }
}
```

Поле `data` инвойса повторяет ответ [Получения инвойса](/ru/docs/invoices/get.md). Поля, которых ещё нет в текущем состоянии, равны `null` или не сериализуются; источником состояния остаются `status` и `is_final`.

## Тело события вывода

Пример: `withdrawal.completed`.

```json
{
  "event_id": "evt_CbYeOgBp9Kt2U3j74fxxLl",
  "event_type": "withdrawal.completed",
  "version": 1,
  "occurred_at": 1739281200,
  "data": {
    "withdrawal_id": "wdr_7K2M9P4Q8R1X5Z3A0bC2dE",
    "external_order_id": "payout-12345",
    "status": "completed",
    "is_final": true,
    "is_test": false,
    "amount": "25.00",
    "fee": "0.50",
    "currency": "USDT",
    "network": "TRC20",
    "destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
    "tx_hash": "7f8a9b0c1d2e3f4a5b6c7d8e9f00112233445566778899aabbccddeeff001122",
    "explorer_url": "https://tronscan.org/#/transaction/7f8a9b0c1d2e3f4a5b6c7d8e9f00112233445566778899aabbccddeeff001122",
    "created_at": 1739277600,
    "completed_at": 1739281200
  }
}
```

Поле `data` вывода повторяет ответ [Получения вывода](/ru/docs/withdrawals/get.md). `tx_hash` и `explorer_url` заполняются, когда в блокчейне появляется отслеживаемая транзакция; они не заменяют поле `status`.

## Тело события депозита платёжного канала

Пример: `payment_channel.deposit.confirmed`.

```json
{
  "event_id": "evt_J7EEYeL9pZJukfj2c5OQ44",
  "event_type": "payment_channel.deposit.confirmed",
  "version": 1,
  "occurred_at": 1767225930,
  "data": {
    "id": "pcd_8ScRvL4jNq2XkB7mTfZdWu",
    "payment_channel_id": "pc_2QhKZv6mRt9WdA3nYpLbXf",
    "project_id": "prj_xFukZuAJZR06pLVBh3uwzv",
    "payment_channel_external_id": "customer-42",
    "status": "confirmed",
    "is_final": true,
    "is_test": false,
    "currency": "USDT",
    "network": "TRC20",
    "chain_id": 728126428,
    "contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "gross": "100",
    "fee": "1",
    "net": "99",
    "applied_fee_percent": 1.0,
    "customer_fee_percent": 0,
    "tx_hash": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
    "transfer_id": "9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25:41a614f803b6fd780986a42c78ec9c7f77e6ded13c:41c9f6a2b7d0138e54ca3b91f6072ed48a5c1e93b7:0",
    "source_address": "TW9s4RkAqBnLpVdX2ChYzUeGm7QfKt3NbZ",
    "destination_address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
    "block_height": 68421905,
    "first_included_block_timestamp": 1767225900,
    "explorer_url": "https://tronscan.org/#/transaction/9f4c1b7e30ad52c8ef6a1d84b0c39f27a5e6d138f4b90c72ae51d63b8407fc25",
    "created_at": 1767225870,
    "updated_at": 1767225930,
    "confirmed_at": 1767225930
  }
}
```

Поле `data` депозита повторяет ответ [Чтения депозита](/ru/docs/payment-channel-deposits/get.md). Источником состояния остаются `status` и `is_final`; `applied_fee_percent` и `customer_fee_percent` — комиссии, зафиксированные для этого депозита, и они не меняются вместе с вашим тарифом.

Все три события депозита несут один и тот же объект, поэтому уже в теле события `confirming` есть канал, идентификатор плательщика, сумма и подтверждения из цепочки. События `confirming` и `reorged` носят уведомительный характер и могут прийти не по порядку — не позволяйте им откатить депозит, о котором вы уже знаете, что он `confirmed`.

## Совместимость

- Новые дополнительные поля могут появляться без смены версии. Игнорируйте поля, которые обработчик не использует.
- Смысл существующих полей не меняется внутри одной версии тела события.
- Идентификаторы и десятичные суммы обрабатывайте как строки. Не разбирайте ID с префиксом как UUID, а суммы — как двоичные числа с плавающей точкой.
- Сначала проверяйте подпись по исходным байтам и только потом разбирайте JSON. Повторная сериализация меняет подписанные данные.
