CChainPay
Увійти

Вебхуки криптоплатежів: перевірка, ретраї та ідемпотентність

Вебхуки — це спосіб, яким ваш бекенд дізнається про надходження платежу. Саме тут найчастіше трапляються помилки інтеграції. Цей гайд описує, як ChainPay їх доставляє і як безпечно обробляти.

Що ви отримуєте

ChainPay надсилає POST із JSON на ваш callbackUrl. Після переказу в TRON payload виглядає так:

{
  "status": "confirmed",
  "externalReference": "order-123",
  "chain": "tron",
  "tokenSymbol": "USDT",
  "tokenAddress": "T…",
  "isNative": false,
  "amount": "25.000000",
  "txHash": "802999e236cf8f…",
  "fee": "1.25",
  "valueForwarded": "23.75",
  "txidOut": "b1c2…"
}

1. Перевіряйте відправника

Вебхуки не підписуються криптографічно. Додайте довгий випадковий секрет у query-рядок callbackUrl і порівнюйте його за сталий час, перш ніж робити будь-що інше. Ніколи не довіряйте amount чи externalReference із неавтентифікованого запиту.

2. Швидко відповідайте 2xx

Повертайте 200 одразу після збереження події. ChainPay чекає на відповідь 10 секунд, будь-який не-2xx статус чи тайм-аут вважає невдачею й не слідує за редіректами. Повільну роботу (листи, виконання замовлення) виносьте в чергу.

3. Очікуйте ретраїв і дублікатів

Невдала доставка повторюється до 8 разів з експоненційною затримкою: від 30 секунд з обмеженням у годину. Доставка — щонайменше раз, тож та сама подія може прийти кілька разів. Використовуйте txHash разом зі status як ключ ідемпотентності й додайте на нього унікальний індекс у базі.

import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.use(express.json());

const SECRET = process.env.WEBHOOK_SECRET;
const processed = new Set(); // use a database unique index in production

app.post('/crypto-webhook', async (req, res) => {
  const given = Buffer.from(String(req.query.token ?? ''));
  const expected = Buffer.from(SECRET);
  if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {
    return res.sendStatus(403);
  }

  const { status, externalReference, amount, txHash } = req.body;
  const key = `${txHash}:${status}`;
  if (processed.has(key)) return res.sendStatus(200); // duplicate delivery

  if (status === 'confirmed') {
    await creditOrder(externalReference, amount); // your logic
  }
  processed.add(key);
  res.sendStatus(200); // any non-2xx makes ChainPay retry
});

app.listen(8080);

4. Зараховуйте лише при confirmed

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

5. Звіряйтеся через status API

Вебхук можна пропустити, якщо сервер був недоступний довше за вікно ретраїв. Вважайте status API джерелом істини й періодично перевіряйте замовлення, які лишаються неоплаченими:

curl https://api.chainpay.site/deposit-address/ADDRESS_ID/status
curl https://api.chainpay.site/deposit-address/ADDRESS_ID/transactions

6. Перевіряйте суму

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

Вимоги до callback URL

callbackUrl має бути публічною HTTP(S)-адресою. Localhost і приватні IP відхиляються при створенні адреси та знову при кожній доставці.

Дивіться також