Разработка callback/webhook уведомлений о платежах

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Разработка callback/webhook уведомлений о платежах
Простой
~2-3 дня
Часто задаваемые вопросы

Направления блокчейн-разработки

Этапы блокчейн-разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1307
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1219
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    920
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1148
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    611
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    886

Разработка 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 по статусам — воркер при пулинге читает только активные записи, не сканирует доставленные.

Безопасность: что обязательно

  • 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 дня.