# Настройка вебхуков

> Как добавить адрес вебхука в кабинете, выбрать события и сохранить секрет, какие приходят заголовки и тело, как устроены 11 повторов, как читать журнал, протестировать адрес и что с редиректами.

## Коротко

Вебхук — это HTTP-запрос, который мы отправляем на ваш сервер при смене статуса счёта. Чтобы его подключить, зайдите в раздел **Интеграции** кабинета и добавьте свой адрес: https://qut.kz/app

В боевом режиме адрес должен быть **только `https`** и на **настоящем домене**. IP-адрес, `localhost` и адреса временных туннелей не принимаются. Секрет показывается **один раз** — скопируйте его сразу.

Ваш адрес должен быть открыт без авторизации: мы приходим как обычный внешний клиент и не передаём ни логинов, ни паролей.

## Как добавить адрес

1. Войдите в кабинет: https://qut.kz/app
2. Откройте раздел **Интеграции**.
3. Добавьте адрес вебхука — полный путь, например `https://site.kz/qutpay-webhook`.
4. Отметьте нужные события.
5. Скопируйте показанный секрет в секретную переменную сервера (например, `QUTPAY_WEBHOOK_SECRET`).
6. Нажмите кнопку проверки и убедитесь, что запрос доходит.

Повторно секрет не показывается. Если потеряли — создайте новый; он начинает действовать с момента создания, поэтому обновите сервер одновременно.

## Требования к адресу

| Требование | Боевой режим | Песочница |
|---|---|---|
| Протокол | Только `https` | Можно `http` |
| Домен | Нужен настоящий домен | Можно `localhost` |
| IP-адрес | Не принимается | Можно |
| Адрес туннеля | Не принимается | Можно |
| Авторизация | Не должно быть | Не должно быть |

Если адрес не проходит требования, ошибка появится сразу при сохранении:

| Код | Что случилось |
|---|---|
| `invalid_url` | Адрес некорректен |
| `webhook_url_requires_https` | HTTP-адрес не принимается |
| `webhook_url_requires_domain` | IP или адрес без домена |
| `webhook_url_tunnel_forbidden` | Адрес временного туннеля |
| `too_many_endpoints` | Превышено число вебхуков |

## Выбор событий

Включать все события не нужно. Чаще всего достаточно двух: `invoice.paid` и `invoice.refunded`.

| Группа | События |
|---|---|
| Счёт | `invoice.created`, `invoice.pending`, `invoice.paid`, `invoice.cancelled`, `invoice.expired`, `invoice.failed`, `invoice.lost` |
| Возвраты | `invoice.refunded`, `invoice.partially_refunded`, `refund.done`, `refund.failed`, `refund.unknown` |
| Общее | `invoice.status` — один канал на любую смену статуса |
| Ошибки | `invoice.create_failed`, `invoice.cancel_failed`, `invoice.bulk` |
| Подписки | `subscription.created`, `subscription.status` |

Какое событие в какой момент приходит: [Жизненный цикл счёта](/kb/ru/invoice-lifecycle).

## Заголовки

В каждом запросе приходят четыре заголовка:

| Заголовок | Что это |
|---|---|
| `X-Webhook-Event` | Название события, например `invoice.paid` |
| `X-Webhook-Timestamp` | Время отправки, Unix-секунды |
| `X-Webhook-Signature` | Префикс `sha256=` и подпись в hex |
| `X-Webhook-Delivery` | Номер доставки, удобно сверять с журналом |

Подпись обязательно проверяйте: [Безопасность вебхуков](/kb/ru/webhook-security).

## Пример тела

```
POST /qutpay-webhook HTTP/1.1
Host: site.kz
Content-Type: application/json
X-Webhook-Event: invoice.paid
X-Webhook-Timestamp: 1757167000
X-Webhook-Signature: sha256=3f1a…
X-Webhook-Delivery: 17

{
  "event": "invoice.paid",
  "invoice": {
    "id": "inv_…",
    "externalOrderId": "1001",
    "status": "paid",
    "amount": 2500,
    "metadata": { "branch": "almaty-1" },
    "receiptUrl": "…",
    "paidAt": "…"
  },
  "sentAt": "…"
}
```

`externalOrderId` и `metadata` — это значения, которые вы передали при создании счёта; они возвращаются без изменений.

## Расписание повторов

Если ваш сервер отвечает **не 2xx** или не отвечает вовсе, мы повторяем запрос **11 раз**. Интервал начинается с 10 секунд и растёт до часа.

Из этого следуют два правила:

1. **Сначала верните 200, работу делайте после.** Долгая обработка приводит к таймауту, а таймаут — к повтору.
2. **Обработка должна быть идемпотентной.** Один и тот же статус по счёту может прийти несколько раз. Выполняйте её один раз по паре `(invoice.id, status)`: [Идемпотентность](/kb/ru/idempotency).

```js
app.post('/qutpay-webhook', express.raw({ type: '*/*' }), async (req, res) => {
  if (!verify(req)) return res.sendStatus(401);
  res.sendStatus(200);                  // сначала ответ
  await enqueue(JSON.parse(req.body));  // потом работа
});
```

## Журнал

В разделе **Интеграции** кабинета записывается каждая отправка: тип события, время, HTTP-ответ вашего сервера. Любая диагностика начинается отсюда.

| Что в журнале | Значение |
|---|---|
| Нет ни одной записи | Мы не отправляли: адрес не добавлен, событие не выбрано или статус ещё не менялся |
| «Отправлено», ответ 401/403 | Адрес закрыт авторизацией |
| Ответ 404 | Неверный путь или маршрут не зарегистрирован |
| Ответ 301/302 | Редирект — мы его не следуем |
| Ответ 500 | Падает ваш обработчик |
| Таймаут | Обработка слишком долгая |

Полная диагностика: [Вебхук не приходит](/kb/ru/webhook-not-arriving).

## Правило редиректов

Редиректы 307 и 308 мы следуем **только на сам этот адрес**: `http` → `https` и вариант со слешем на конце. Редирект, ведущий в другое место, мы не следуем.

Причина простая: слепое следование редиректу может отправить запрос на чужой адрес. Поэтому указывайте в кабинете **конечный** адрес, а не промежуточный.

## Как протестировать

1. Отправьте пробный запрос кнопкой проверки в кабинете — так вы увидите, что ваш код проверки подписи работает.
2. Перейдите в песочницу, создайте счёт и симулируйте оплату через `POST /api/v1/invoices/{id}/simulate {"status":"paid"}`. Вебхуки при этом отправляются по-настоящему.
3. Чтобы проверить повторы, временно заставьте обработчик отвечать 500 и посмотрите в журнале, как приходят повторные попытки.

Полный сценарий: [Как протестировать интеграцию](/kb/ru/testing-integration).

## Вопросы и ответы

**Можно добавить несколько адресов?** Да. Например, один на сайт, другой на внутренний мониторинг. У каждого свой набор событий.

**Достаточно ли одного вебхука?** В большинстве случаев да. Если покупатель стоит у устройства и ждёт результата, опрашивайте статус параллельно: [Вебхук или опрос статуса](/kb/ru/polling-vs-webhook).

**Потерял секрет.** Создайте новый в кабинете и одновременно замените значение на сервере.

**Я сменил адрес — что будет со старыми событиями?** События, стоящие в очереди на повтор, уйдут на новый адрес.

**В песочнице вебхуки приходят?** Да, отправляются по-настоящему. Разница лишь в том, что адрес может быть `http` и `localhost`.
