Розробка callback/webhook уведомлень про платежі
Платіж зафіксований у блокчейні — означає, його треба обробити у вашій системі. Проблема: блокчейн не може "дзвонити" у ваш бекенд. Є два підходи: polling (ви запитуєте) та push (зовнішня система повідомляє вас). Webhook — це push-уведомлення від платіжного процесора або вашого власного моніторингу блокчейну.
Архітектура webhook-системи
Типова схема для крипто-платежів:
Blockchain → Monitoring service → Webhook dispatcher → Your app endpoint
↓
Retry queue (Redis/DB)
Monitoring service — це або стороння послуга (Alchemy Notify, Moralis Streams, QuickNode Streams), або ваш власний процес, що слухає ноду.
Отримання webhook від Alchemy Notify
// Створення webhook через Alchemy API
const response = await fetch('https://dashboard.alchemy.com/api/create-webhook', {
method: 'POST',
headers: {
'X-Alchemy-Token': process.env.ALCHEMY_AUTH_TOKEN!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
network: 'ETH_MAINNET',
webhook_type: 'ADDRESS_ACTIVITY',
webhook_url: 'https://yourapp.com/webhooks/crypto',
addresses: ['0xYourAddress'],
}),
})
Ваш webhook endpoint: правильна обробка
Головне правило: webhook endpoint повинен відповідати 200 якомога швидше. Всю важку логіку — в чергу:
// Express.js endpoint
app.post('/webhooks/crypto', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. Верифікувати підпис — ДО будь-якої обробки
const signature = req.headers['x-alchemy-signature'] as string
const isValid = verifyAlchemySignature(req.body, signature, process.env.WEBHOOK_SIGNING_KEY!)
if (!isValid) {
return res.status(401).send('Invalid signature')
}
// 2. Відповісти 200 негайно
res.status(200).send('OK')
// 3. Поставити в чергу для асинхронної обробки
const payload = JSON.parse(req.body.toString())
await jobQueue.add('process-crypto-payment', payload, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
})
})
function verifyAlchemySignature(body: Buffer, signature: string, signingKey: string): boolean {
const hmac = crypto.createHmac('sha256', signingKey)
hmac.update(body)
const digest = hmac.digest('hex')
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))
}
timingSafeEqual обов'язковий — без нього timing attack дозволить підібрати підпис за розумний час.
Ідемпотентність: обробляти повторення коректно
Webhook-провайдери гарантують доставку at-least-once, не exactly-once. Ваш обробник повинен бути ідемпотентним:
async function processPaymentWebhook(txHash: string, address: string, amountWei: bigint) {
// Використовуємо INSERT ... ON CONFLICT DO NOTHING
const result = await db.query(`
INSERT INTO processed_webhooks (tx_hash, processed_at)
VALUES ($1, NOW())
ON CONFLICT (tx_hash) DO NOTHING
RETURNING id
`, [txHash])
if (result.rowCount === 0) {
// Вже оброблено — пропускаємо, не помилка
return
}
// Основна логіка обробки платежу
await updatePaymentStatus(address, amountWei, txHash)
}
Retry-механізм для вихідних webhook (якщо ваш сервіс повідомляє інші)
Якщо ваш сервіс сам відправляє webhook в системи клієнтів:
interface WebhookDelivery {
id: string
url: string
payload: object
attempt: number
nextRetryAt: Date
}
async function deliverWebhook(delivery: WebhookDelivery): Promise<void> {
try {
const res = await fetch(delivery.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signPayload(delivery.payload),
'X-Webhook-ID': delivery.id,
'X-Webhook-Attempt': String(delivery.attempt),
},
body: JSON.stringify(delivery.payload),
signal: AbortSignal.timeout(10_000), // 10 секунд таймаут
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
await db.markDelivered(delivery.id)
} catch (err) {
const nextAttempt = delivery.attempt + 1
if (nextAttempt > 10) {
await db.markFailed(delivery.id, String(err))
return
}
// Експоненціальний backoff: 30s, 1m, 2m, 5m, 10m, ...
const delayMs = Math.min(30_000 * Math.pow(2, nextAttempt - 1), 3_600_000)
await db.scheduleRetry(delivery.id, nextAttempt, new Date(Date.now() + delayMs))
}
}
Схема retry: 10 спроб з експоненціальним backoff достатньо для більшості випадків. Фінальний провал — повідомити розробникам, зберегти у dead letter queue.
Таблиця для зберігання доставок
CREATE TABLE webhook_deliveries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type VARCHAR(50) NOT NULL,
payload JSONB NOT NULL,
target_url TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
attempt INTEGER DEFAULT 0,
next_retry_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
delivered_at TIMESTAMPTZ,
last_error TEXT
);
CREATE INDEX ON webhook_deliveries(status, next_retry_at)
WHERE status IN ('pending', 'retrying');
Partial index за статусами — воркер при polling читає лише активні записи, не сканує доставлені.
Безпека: що обов'язково
- HMAC-SHA256 підпис на кожен вихідний webhook з секретом клієнта
-
Timestamp у payload (поле
sent_at) та перевірка на стороні отримувача, що webhook не старше 5 хвилин — захист від replay attacks - HTTPS only — не доставляти на HTTP endpoints
-
Ідемпотентний ключ у заголовку (
X-Webhook-ID) — отримувач може дедуплікувати
Строки реалізації: базовий webhook endpoint з верифікацією підпису та чергою — 1 день. Повнофункціональна система з retry, dead letter queue, dashboard для моніторингу доставок, управлінням підписками — 2–3 дні.







