Вебхуки криптоплатежів: перевірка, ретраї та ідемпотентність
Вебхуки — це спосіб, яким ваш бекенд дізнається про надходження платежу. Саме тут найчастіше трапляються помилки інтеграції. Цей гайд описує, як 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…"
}
status—pending(лише якщо ви ввімкнули ранні сповіщення черезpending: true) абоconfirmed.tokenSymbol,tokenAddressтаisNativeописують актив, який справді надійшов; це важливо для мультитокенних адрес.fee,valueForwardedіtxidOut(транзакція виплати) присутні лише тоді, коли результат переказу відомий на момент сповіщення — наразі це TRON. В EVM-мережахconfirmed-вебхук надсилається при досягненні порога підтверджень і цих полів не містить.
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 відхиляються при створенні адреси та знову при кожній доставці.