# Перевірка й тестування (/uk/webhooks/receiving)



Перевіряйте кожен вебхук перед використанням його payload, а потім надішліть тест, щоб переконатися, що ваш обробник працює від початку до кінця.

## Перш ніж почати [#before-you-start]

* [Збережена цільова адреса](/webhooks/configuration#save-a-destination) та її signing secret (ключ підпису) `twhsec_...`. Не використовуйте свій Server Secret Token для перевірки вебхуків.
* Маршрут обробника, який може зчитати початкове тіло запиту, перш ніж будь-який парсер JSON змінить його.
* Сховище з обмеженням унікальності для `id` події, щоб дублікати були безпечними.

## Отримайте вебхук [#receive-a-webhook]

<div className="fd-steps [&_h3]:fd-step">
  ### Зчитайте початкове тіло [#read-the-raw-body]

  Tapaya надсилає JSON `POST` із такими заголовками. Зберігайте тіло як початкові байти; розібране й повторно серіалізоване тіло вже не відповідає підпису.

  ```http
  Content-Type: application/json
  X-Tapaya-Event-Id: 019a0000-0000-7000-8000-000000000001
  X-Tapaya-Signature: t=1793361600,v1=<lowercase hexadecimal HMAC-SHA256>
  ```

  * В Express використовуйте для цього маршруту `express.raw({ type: "application/json" })` перед глобальним парсером JSON.

  ### Перевірте підпис [#check-the-signature]

  Обчисліть HMAC-SHA256 для часової позначки, крапки та початкового тіла, а потім порівняйте результат із `v1` за допомогою порівняння зі сталим часом виконання.

  ```js title="verify-tapaya-webhook.mjs"
  import { createHmac, timingSafeEqual } from "node:crypto";

  export function verifyTapayaWebhook(rawBody, signatureHeader, secret, now = Date.now()) {
    if (!Buffer.isBuffer(rawBody) || typeof signatureHeader !== "string") {
      throw new Error("Missing raw body or signature");
    }
    if (typeof secret !== "string" || !secret.startsWith("twhsec_")) {
      throw new Error("Missing signing secret");
    }

    const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader);
    if (!match) throw new Error("Malformed signature");

    const timestamp = Number(match[1]);
    if (!Number.isSafeInteger(timestamp) || Math.abs(now / 1000 - timestamp) > 300) {
      throw new Error("Expired signature");
    }

    const expected = createHmac("sha256", Buffer.from(secret, "utf8"))
      .update(`${match[1]}.`, "utf8")
      .update(rawBody)
      .digest();
    const received = Buffer.from(match[2], "hex");
    if (!timingSafeEqual(expected, received)) throw new Error("Invalid signature");

    return JSON.parse(rawBody.toString("utf8"));
  }
  ```

  * Використовуйте повний ключ `twhsec_...` як ключ UTF-8. Не декодуйте його та не видаляйте префікс.
  * Відхиляйте часові позначки, віддалені від вашого годинника більш ніж на п'ять хвилин, і підтримуйте синхронізацію годинника.
  * Поки ви [виконуєте ротацію signing secret](/webhooks/configuration#configuration-changes), спочатку перевіряйте новий ключ, потім старий, доки черга не спорожніє.

  ### Збережіть і підтвердьте [#store-and-acknowledge]

  Приймайте подію лише після того, як її надійно збережено, щоб збій на вашому боці запускав повторну спробу.

  ```js title="server.mjs"
  import express from "express";
  import { verifyTapayaWebhook } from "./verify-tapaya-webhook.mjs";
  import { saveEventOnce } from "./events.mjs"; // your store, unique on event id

  const app = express();

  app.post(
    "/webhooks/tapaya",
    express.raw({ type: "application/json" }),
    async (req, res) => {
      let event;
      try {
        event = verifyTapayaWebhook(
          req.body,
          req.get("X-Tapaya-Signature"),
          process.env.TAPAYA_WEBHOOK_SECRET,
        );
      } catch {
        return res.sendStatus(400);
      }
      if (event.organizationId !== process.env.TAPAYA_ORGANIZATION_ID) {
        return res.sendStatus(400);
      }

      await saveEventOnce(event); // skips an id it already stored
      res.sendStatus(204);
    },
  );
  ```

  * Повертайте `2xx` для події, яку ви вже зберегли. Повторні спроби зберігають той самий `id` і тіло, але мають нові часову позначку та підпис.
  * Повертайте відповідь поза `2xx`, якщо не можете зберегти нову подію, щоб доставка повторила спробу.
  * Обробляйте збережені події з власної черги. Порядок доставки не гарантовано, тому звіряйте старіші знімки з поточним станом.
</div>

## Надішліть тестовий вебхук [#send-a-test-webhook]

Тестова дія в Platform надсилає один підписаний запит `webhook.test` на збережену URL-адресу. Вона працює, навіть якщо вебхуки вимкнено або типу немає у вашій підписці, і не створює платежу, повернення коштів або мерчанта.

1. Відкрийте сторінку **API Keys** своєї організації та збережіть цільову адресу.
2. Скористайтеся тестовою дією вебхука.
3. Перевірте результат, тривалість і надісланий JSON, які показує Platform.

```json
{
  "id": "019a0000-0000-7000-8000-000000000010",
  "type": "webhook.test",
  "apiVersion": "1",
  "organizationId": "019a0000-0000-7000-8000-000000000002",
  "merchantId": null,
  "createdAt": "2026-10-30T12:00:00.000000Z",
  "test": true,
  "data": {
    "message": "This is a test webhook from Tapaya. No payment was created."
  }
}
```

<Accordions type="multiple">
  <Accordion title="Що перевіряє тест" id="what-a-test-covers">
    * Одна спроба з граничним часом 10 секунд і без автоматичних повторів.
    * Тест перевіряє доставку та обробку підпису, але не payload платежів або онбордингу. Тестуйте їх за допомогою дій у пісочниці (sandbox).
    * Невдала доставка відображається як результат тесту, а не як помилка в Platform.
  </Accordion>

  <Accordion title="Ліміти тестів" id="test-limits">
    * Доставка з черги та ручні тести мають спільний ліміт: 2 одночасні запити на організацію.
    * Ручні тести також дозволяють 10 запусків за будь-які останні 60 секунд на організацію, спільно для всіх користувачів.
    * У разі перевищення будь-якого ліміту тест повертає `429` із `Retry-After` у секундах і нічого не надсилає.
    * Прийняті тести враховуються, навіть якщо вони завершилися помилкою або їх скасовано. Повторне створення цільової адреси не скидає лічильник.
  </Accordion>
</Accordions>

## Локальні обробники й обробники автоматизації [#local-and-automation-receivers]

Спрямуйте вебхук на машину для розробки або на no-code-інструмент, поки ваш робочий сервер не готовий.

<Accordions type="multiple">
  <Accordion title="Протестуйте локальний обробник" id="test-a-local-receiver">
    * Збережіть URL-адресу публічного HTTPS-тунелю як цільову адресу; URL-адреси localhost і приватні IP-адреси відхиляються.
    * Залишайте перевірку підпису ввімкненою й переконайтеся, що тунель передає початкове тіло та заголовки Tapaya без змін.
    * Якщо URL-адреса тунелю зміниться, знову збережіть цільову адресу. Залишайте старий тунель доступним, доки події, що стоять у черзі на нього, не будуть доставлені.
  </Accordion>

  <Accordion title="Підключіть інструмент автоматизації" id="connect-an-automation-tool">
    * Точка входу не повинна потребувати власних заголовків автентифікації, має зберігати початкове тіло й відповідати протягом 10 секунд.
    * Вона має самостійно перевіряти HMAC початкового тіла або бути розташованою за адаптером перевірки, який це робить.
    * Щойно інструмент відповість `2xx`, доставка вважається успішною. Подальші збої робочого процесу та дублікати обробляє сам інструмент; вони не запускають наступної доставки.
  </Accordion>
</Accordions>

## Обробка помилок [#handle-errors]

<Accordions type="multiple">
  <Accordion title="Результати тестів" id="test-results">
    | Результат         | Значення                                                                                              |
    | ----------------- | ----------------------------------------------------------------------------------------------------- |
    | `delivered: true` | Обробник повернув HTTP `2xx`.                                                                         |
    | `http_error`      | Обробник повернув статус поза `2xx`.                                                                  |
    | `transport_error` | Не вдалося з'єднатися з цільовою адресою, зокрема через збої TLS, DNS або перевірки публічної мережі. |
    | `timeout`         | Обробник не підтвердив отримання протягом 10 секунд.                                                  |
  </Accordion>

  <Accordion title="Перевірка підпису" id="signature-checks">
    | Ознака                  | Що перевірити                                                                                                               |
    | ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
    | Невідповідність підпису | Використовуйте повний ключ і початкові байти. Переформатований JSON або API-токен як ключ призведуть до невдалої перевірки. |
    | Прострочений підпис     | Синхронізуйте годинник обробника та перевіряйте за заголовком запиту, а не за `createdAt`.                                  |
  </Accordion>

  <Accordion title="Доставка" id="delivery">
    | Ознака                                                 | Що перевірити                                                                                                                                  |
    | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
    | URL-адресу відхилено або підключення заблоковано       | Використовуйте публічний HTTPS на порту 443 без переспрямувань, облікових даних або фрагмента.                                                 |
    | Немає події вебхука                                    | Перевірте організацію, стан увімкнення та вибрані типи подій. Минулі зміни не додаються заднім числом.                                         |
    | Дублікат або старіша подія                             | Виконуйте дедуплікацію за `id` і звіряйте з поточним станом замість припущень щодо порядку. Див. [правила доставки](/webhooks#delivery-rules). |
    | Тест працює, але обробка платежів завершується невдало | `webhook.test` має іншу структуру `data`. Маршрутизуйте за `type` і тестуйте за допомогою справжніх подій пісочниці.                           |
  </Accordion>
</Accordions>
