# Вебхуки: как xpayment уведомляет ваш сервер о платежах

Что такое webhooks xpayment, когда они срабатывают, как их настроить и как обрабатывать события платежей и возвратов.

## Что такое вебхук?

**Вебхук** — это HTTP-запрос, который xpayment отправляет на **ваш сервер**, когда с платежом что-то происходит. Например, когда покупатель оплатил счёт или отклонил его.

Представьте разницу между двумя подходами:

- **Вы сами спрашиваете** (polling) — ваш сервер периодически отправляет `GET /v1/payments/{id}` и проверяет: «А как там платёж?». Это работает, но создаёт лишнюю нагрузку и задержку.
- **Вас уведомляют** (webhook) — как только статус платежа меняется, xpayment сам приходит к вам с `POST /your-endpoint` и говорит: «Эй, вот что случилось».

Вебхук — это **push-уведомление для серверов**.

---

## Какую роль играет вебхук в платёжном потоке?

Вебхук — это завершающее звено в обоих платёжных сценариях xpayment:

### Прямой платёж (`POST /v1/payments`)

```
Ваш сервер → xpayment → Kaspi (push на телефон покупателя)
                                     ↓ покупатель оплачивает
Ваш сервер ← xpayment (webhook) ← Kaspi (меняет статус)
```

**Шаг 7 из 8** в потоке прямого платежа — именно здесь срабатывает вебхук. Без него ваш сервер не знает, когда выполнять заказ.

### Платёжная ссылка (`POST /v1/payments/link`)

```
Ваш сервер → создаёт ссылку → покупатель открывает в Kaspi
                                     ↓ покупатель оплачивает
Ваш сервер ← xpayment (webhook) ← Kaspi (меняет статус)
```

**Шаг 6 из 6** — без вебхука вы не узнаете, что покупатель перешёл по ссылке и оплатил.

---

## Зачем вебхук, если есть GET /payments/{id}?

| | Polling (GET) | Webhook (POST) |
|---|---|---|
| **Задержка** | До 15 сек (интервал вашего поллера) | Секунды после оплаты |
| **Нагрузка** | Постоянные лишние запросы | Только при событии |
| **Масштаб** | Сложно при тысячах платежей | Не зависит от объёма |
| **Пропуск событий** | Можно пропустить при сбое | Повтор при ошибке |

Polling подходит как **резервный** механизм. Вебхук — как **основной**.

> xpayment использует свой внутренний поллер (каждые 15 сек) для опроса Kaspi — и именно тогда срабатывает доставка вебхука на ваш сервер.

## Как добавить вебхук в xpayment

### Что нужно перед настройкой

- **Публичный HTTPS-эндпоинт** — URL, доступный из интернета, на который xpayment будет отправлять POST-запросы. Локальный `localhost` не подойдёт.
- **Аккаунт xpayment** — войдите через Google на [xpayment.kz](https://xpayment.kz)

---

### Шаг 1 — Откройте раздел Вебхуки

В боковом меню личного кабинета нажмите **«Вебхуки»** (или перейдите по адресу `/app/webhooks`). Вы увидите список существующих подписок (пустой, если вебхуков ещё нет).

---

### Шаг 2 — Создайте новый вебхук

Нажмите кнопку **«Создать вебхук»**. Откроется модальное окно с двумя вкладками:

#### Вариант А — Быстрый запуск (рекомендуется)

Если вы используете одну из платформ-партнёров xpayment (например, [tec.delivery](https://tec.delivery)) — выберите её карточку. URL и настройки заполнятся автоматически.

#### Вариант Б — Ввести вручную

Заполните поля самостоятельно:

| Поле | Описание |
|---|---|
| **URL** | Полный HTTPS-адрес вашего эндпоинта. Например: `https://api.myshop.kz/webhooks/xpayment` |
| **Описание** | Необязательное название — для вашего удобства |
| **Секрет** | Случайная строка для подписи запросов (необязательно, но рекомендуется) |

---

### Шаг 3 — Выберите события

Отметьте галочками, о каких событиях вы хотите получать уведомления. Рекомендуем выбрать **все 5 событий** — так ваш сервер всегда будет в курсе любых изменений:

- `payment.completed` — платёж прошёл ✅
- `payment.cancelled` — платёж отменён ❌
- `payment.failed` — платёж не прошёл ⚠️
- `payment.refunded` — возврат выполнен 🔄
- `payment.refund_failed` — возврат не удался ⚠️

---

### Шаг 4 — Сохраните

Нажмите **«Создать»**. Вебхук появится в списке со статусом **«Активен»**. С этого момента xpayment будет отправлять POST-запрос на ваш URL при каждом выбранном событии.

---

> **Совет:** Для тестирования можно использовать сервисы вроде [webhook.site](https://webhook.site) — они дают временный публичный URL и показывают входящие запросы в реальном времени.

На диаграмме ниже показан интерфейс создания вебхука в xpayment:

## Типы событий

xpayment поддерживает **6 типов событий** — по одному для каждого возможного исхода платежа или возврата.

---

### `payment.completed`

**Когда:** Покупатель нажал «Оплатить» в приложении Kaspi и транзакция прошла успешно.

**Что делать на вашем сервере:**
- Подтвердить и выполнить заказ
- Открыть доступ к продукту / сервису
- Отправить покупателю чек или подтверждение

**Это главное событие** — именно его ждёт большинство интеграций.

---

### `payment.expired`

**Когда:** Покупатель не оплатил QR-код до истечения его срока действия (обычно 15 минут).

**Что делать:**
- Освободить зарезервированный товар / место
- Уведомить покупателя и предложить создать новый платёж
- Не повторять платёж автоматически — пользователь должен инициировать его сам

---

### `payment.cancelled`

**Когда:** Платёж был отменён — либо вашим сервером (через `POST /v1/payments/{id}/cancel`), либо покупатель нажал «Отклонить счёт» в Kaspi.

**Что делать:**
- Освободить зарезервированный товар / место
- Уведомить покупателя, если нужно
- Не списывать деньги — платёж отменён

---

### `payment.failed`

**Когда:** Kaspi отклонил платёж — недостаточно средств, неверный номер телефона, технический сбой на стороне Kaspi.

**Что делать:**
- Зафиксировать причину сбоя
- Предложить покупателю повторить попытку или выбрать другой способ оплаты
- Не повторять тот же платёж автоматически без участия пользователя

---

### `payment.refunded`

**Когда:** Возврат по завершённому платежу был успешно выполнен через `POST /v1/refunds`.

**Что делать:**
- Обновить статус заказа на «Возвращён»
- Отправить покупателю уведомление о возврате
- Обновить финансовые записи

---

### `payment.refund_failed`

**Когда:** Попытка возврата завершилась ошибкой — например, Kaspi не смог провести обратную транзакцию.

**Что делать:**
- Уведомить команду поддержки — возврат нужно провести вручную
- Зафиксировать `refund_id` для расследования
- Связаться с покупателем: деньги **не были возвращены**

---

## Таблица событий

| Событие | Триггер | Действие |
|---|---|---|
| `payment.completed` | Покупатель оплатил | Выполнить заказ |
| `payment.expired` | QR-код истёк | Освободить резерв, предложить новый платёж |
| `payment.cancelled` | Отменён вами или покупателем | Освободить резерв |
| `payment.failed` | Kaspi отклонил платёж | Попросить повторить |
| `payment.refunded` | Возврат прошёл | Обновить статус заказа |
| `payment.refund_failed` | Возврат не прошёл | Ручная проверка |

На диаграмме ниже показан путь от смены статуса в Kaspi до доставки вебхука на ваш сервер:

## Формат payload

Когда событие срабатывает, xpayment отправляет `POST`-запрос на ваш URL с таким телом:

```json
{
  "event": "payment.completed",
  "event_version": "1",
  "delivery_id": "c09a3647-7822-4899-8401-3857cbd5bf68",
  "attempt_number": 1,
  "source": "xpayment",
  "payment_id": "65cd0d84-5f66-4763-94d2-1b8e1ba68347",
  "merchant_order_id": "order-5512",
  "created_at": "2026-04-17T10:32:00+05:00"
}
```

**Поля:**

| Поле | Тип | Описание |
|---|---|---|
| `event` | string | Тип события, например `payment.completed` |
| `event_version` | string | Версия схемы payload (сейчас `1`) |
| `delivery_id` | string | Уникальный ID доставки — используйте для идемпотентности |
| `attempt_number` | int | Номер попытки доставки, начинается с `1` и растёт при повторах |
| `source` | string | Всегда `xpayment` |
| `payment_id` | string | ID платежа, к которому относится событие |
| `merchant_order_id` | string | Ваш ID заказа (отсутствует, если вы его не задавали) |
| `created_at` | string | Время события (ISO 8601) |

---

## Заголовки запроса

xpayment отправляет следующие HTTP-заголовки:

```http
POST /webhooks/xpayment HTTP/1.1
Content-Type: application/json
User-Agent: Go-http-client/2.0
X-xPayment-Signature: aeafc0dcd86de4d4d376f525be2b1175520c2818c93938ec8d069957c63a373b
```

`X-xPayment-Signature` — это HMAC-SHA256 от тела запроса (см. раздел [Безопасность](#security) ниже).

---

## Повтор при ошибке

Если ваш эндпоинт вернул **не-2xx статус** (например, `500 Internal Server Error` или `503 Service Unavailable`), xpayment пометит доставку как неудачную и попытается повторить её позже (всего до 3 попыток; `attempt_number` увеличивается каждый раз).

**Рекомендации:**
- Возвращайте `200 OK` как можно быстрее — обработку ведите асинхронно
- Не возвращайте `5xx` просто потому что ещё не обработали событие
- Если получили событие, которое не ожидали — всё равно верните `200`

---

## Идемпотентность

Иногда одно и то же событие может быть доставлено **дважды** — при повторе после тайм-аута или сбоя сети. Защита:

1. **Проверяйте `delivery_id`** — сохраняйте полученные `delivery_id`. Если такой `delivery_id` уже обработан — пропустите.
2. **Проверяйте `payment_id`** — ID платежа тоже уникален. Если платёж со статусом `COMPLETED` уже записан — не выполняйте заказ повторно.

```typescript
// Пример проверки идемпотентности
const deliveryId = payload.delivery_id
if (await alreadyProcessed(deliveryId)) {
  return res.status(200).json({ ok: true }) // acknowledge without processing
}

await markAsProcessed(deliveryId)
await fulfillOrder(payload.merchant_order_id)
```

---

## Безопасность

Каждая доставка вебхука **подписывается**. xpayment вычисляет **HMAC-SHA256** от точного «сырого» тела запроса с ключом — вашим секретом подписки (`secret`) — и отправляет результат в виде hex в нижнем регистре в заголовке `X-xPayment-Signature` (чистый hex — **без префикса `sha256=`**).

Чтобы доверять входящему вебхуку:

- **Проверяйте подпись** — пересчитайте `HMAC-SHA256(secret, raw_body)` и сравните с `X-xPayment-Signature`, используя сравнение за постоянное время. Подписывайте байты ровно в том виде, в каком они пришли — не сериализуйте заново распарсенный JSON, иначе пробелы и порядок ключей изменят хеш. Полные примеры кода (Node, Go, Python) — в статье [Безопасность вебхуков и секрет](https://xpayment.kz/blog/webhook-security).
- **HTTPS обязателен** — никогда не принимайте вебхуки по HTTP.
- **IP-фильтрация** (опционально) — дополнительно ограничьте запросы IP-адресами серверов xpayment.

---

## Смотрите также

- [Безопасность вебхуков и секрет](https://xpayment.kz/blog/webhook-security) — проверка подписи и управление секретом
- [Прямые платежи](https://xpayment.kz/blog/direct-payment) — как создать платёж, который запустит `payment.completed`
- [Платёжные ссылки](https://xpayment.kz/blog/payment-link) — второй сценарий, где вебхуки завершают цикл
- [Подключение устройства](https://xpayment.kz/blog/device-setup) — необходимо перед любыми платежами

## Читайте также
- [Безопасность вебхуков](https://xpayment.kz/blog/webhook-security)
- [Удалённые платежи](https://xpayment.kz/blog/direct-payment)
- [Платёжные ссылки](https://xpayment.kz/blog/payment-link)

---

Источник: https://xpayment.kz/blog/webhooks
