# Вебхуки

Отримуйте оновлення платежів, перевіряйте підписи та обробляйте повтори.

Source: https://docs.invoise.me/uk/integration/webhooks/

Вебхук — це HTTP-запит, який Invoise надсилає на ваш сервер, коли змінюється платіж. Можна почати з [перевірок статусу через GET](https://docs.invoise.me/uk/payments/status/) і додати вебхуки, коли знадобляться автоматичні оновлення.

## 1. Зареєструйте свій ендпоінт

```bash
curl -X POST 'https://platform.invoise.me/api/v1/shops/{shop_id}/webhooks' \
  -H 'Authorization: Bearer ivk_...' \
  -H 'Idempotency-Key: <saved-unique-key>' \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/invoise","filters":["transfer","invoice","payout"]}'
```

Використовуйте власну публічну HTTPS-адресу. Локальні та приватні адреси відхиляються. Збережіть `secret`, повернутий під час створення ендпоінту: він потрібен для перевірки запитів.

У магазину може бути до 10 ендпоінтів. URL ендпоінту можна змінити, тож використовуйте наявний замість додавання нового. Див. [Ліміти](https://docs.invoise.me/uk/integration/idempotency-and-errors/#ліміти).

## 2. Оберіть потрібні події

| Подія | Значення |
| --- | --- |
| `transfer.observed` | Переказ помічено; він ще не підтверджений. |
| `transfer.confirmed` | Вхідний переказ має достатньо підтверджень. Це може бути лише частковий платіж за рахунком. |
| `transfer.reverted` | Попередню подію переказу відкотили. |
| `invoice.closed` | Рахунок закрився в мережі. |
| `invoice.cancelled` | Сторінку оплати скасовано або в рахунку минув термін; це не повернення коштів. |
| `payout.sent` | Виплату розіслано в мережу або помічено її вихідний переказ. |
| `payout.confirmed` | Доступний повний облік розрахунку. |
| `payout.reverted` | Попереднє підтвердження виплати відкотили. |
| `payout.failed` | Остаточна невдача виплати потребує втручання. |

Для `invoice.cancelled` `data.reason` пояснює причину: `expired`, коли скінчився [термін дії рахунку](https://docs.invoise.me/uk/payments/deposits-and-invoices/#термін-дії-рахунку), `merchant_blocked`, коли персонал Invoise заблокував мерчанта. Скасування мерчантом не має `reason`.

Підписуйтеся за групами: `transfer`, `invoice`, `payout`. Вхідні перекази та вихідні виплати — різні події. Тимчасова затримка RPC чи газу — це не `payout.failed`.

`gas.wait` і `sweep.failed` приймаються як фільтри, але Invoise не надсилає їх на ваш ендпоінт. Це внутрішні сигнали: `gas.wait` означає, що виплата очікує кошти на мережеву комісію й продовжується сама; `sweep.failed` означає, що спроба перемістити кошти вашому отримувачу не вдалася. Для жодного з них дія не потрібна. Виплата, яка не може завершитися, доходить до вас як `payout.failed`.

У магазині-пісочниці можна симулювати обидва через `POST /shops/{shop_id}/sandbox/simulate`, щоб перевірити, що затримана виплата не ламає ваш процес. Вони змінюють лише стан виплати й не надсилають вебхук.

## 3. Перевірте перед обробкою

`Invoise-Signature` має форму `t=<timestamp>,v1=<hex>`. Підпис — це HMAC-SHA256 від `timestamp + "." + raw_body`, з використанням вашого секрету вебхука.

Наступний приклад на Node.js також відхиляє часові мітки, що відрізняються від годинника сервера більш ніж на п'ять хвилин. Тримайте цей годинник синхронізованим і свідомо обирайте свій допуск.

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret) {
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? '');
  if (!match) return false;
  const [, timestamp, signature] = match;
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  return timingSafeEqual(Buffer.from(signature, 'hex'), expected);
}
```

Передавайте оригінальні байти запиту як `rawBody`. Не розбирайте JSON і не перетворюйте його назад у рядок перед перевіркою підпису. Відхиляйте неправильні підписи до збереження чи дії за їхнім вмістом.

## 4. Обробляйте один раз

Скорочений вміст виглядає так:

```json
{
  "id": "<event-id>",
  "type": "transfer.confirmed",
  "created_at": "2026-09-19T10:00:00Z",
  "data": {
    "issuance_id": "<issuance-id>",
    "shop_id": "<shop-id>",
    "external_id": "order-123",
    "type": "invoice",
    "invoice_cancelled": false
  }
}
```

| Випадок | Обробка |
| --- | --- |
| Дублікати | Усувайте дублікати за `Invoise-Event-ID`, який збігається з `id` конверта. Зберігайте ID і бізнес-оновлення атомарно. |
| Прийняття події | Зберігайте прийняті події надійно, повертайте 2xx одразу, а чергу обробляйте потім. |
| Повтори й відтворення | Повтори й ручні відтворення повторюють той самий ID події. `Invoise-Attempt-ID` змінюється і не є ключем усунення дублікатів. |
| Порядок подій | Події можуть надходити не по порядку. Використовуйте їхній вміст і за потреби звіряйте з [поточним статусом](https://docs.invoise.me/uk/payments/status/). |

Для відкатів `chain_event.reference_event_id` вказує на попередню подію мережі. Дані виплати містять стабільний `payout_id`, хеш транзакції, суми та деталі комісії; невідомі суми — `null`. Список ID вхідних переказів у них обмежено 100 записами, з позначкою усічення.

Старіші збережені події можуть використовувати `project_id`; спершу читайте `shop_id`, а за потреби повертайтеся до нього. Відтворення зберігають оригінальні байти.

## Перегляд невдалої доставки

Список: `GET /api/v1/shops/{shop_id}/deliveries`. Повторіть доставку через `POST /api/v1/shops/{shop_id}/deliveries/{id}/replay` зі збереженим ключем ідемпотентності. Повтор не створює нову бізнес-подію.
