Payment Webhook/Callback Notification System

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Payment Webhook/Callback Notification System
Simple
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1217
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1046
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Developing Payment Callback/Webhook Notifications

A payment is confirmed on the blockchain — which means you need to process it in your system. The problem: the blockchain can't "call" your backend. There are two approaches: polling (you ask) and push (an external system notifies you). A webhook is a push notification from a payment processor or your own blockchain monitoring service.

Webhook System Architecture

Typical scheme for crypto payments:

Blockchain → Monitoring service → Webhook dispatcher → Your app endpoint
                                        ↓
                                  Retry queue (Redis/DB)

The monitoring service is either a third-party provider (Alchemy Notify, Moralis Streams, QuickNode Streams) or your own process listening to the node.

Receiving Webhooks from Alchemy Notify

// Create webhook via 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'],
  }),
})

Your Webhook Endpoint: Proper Handling

Main rule: webhook endpoint must respond 200 as quickly as possible. All heavy logic — in the queue:

// Express.js endpoint
app.post('/webhooks/crypto', express.raw({ type: 'application/json' }), async (req, res) => {
  // 1. Verify signature — BEFORE any processing
  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. Reply with 200 immediately
  res.status(200).send('OK')

  // 3. Put in queue for async processing
  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 is mandatory — without it, timing attacks allow signature guessing in reasonable time.

Idempotency: Handle Retries Correctly

Webhook providers guarantee at-least-once delivery, not exactly-once. Your handler must be idempotent:

async function processPaymentWebhook(txHash: string, address: string, amountWei: bigint) {
  // Use 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) {
    // Already processed — skip, not an error
    return
  }

  // Main payment processing logic
  await updatePaymentStatus(address, amountWei, txHash)
}

Retry Mechanism for Outgoing Webhooks (if your service notifies others)

If your service sends webhooks to client systems:

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 second timeout
    })

    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
    }

    // Exponential 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 scheme: 10 attempts with exponential backoff is sufficient for most cases. Final failure — notify developers, save to dead letter queue.

Table for Storing Deliveries

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 by status — worker when polling reads only active records, doesn't scan delivered ones.

Security: What's Required

  • HMAC-SHA256 signature on each outgoing webhook with client secret
  • Timestamp in payload (sent_at field) and server-side check that webhook is not older than 5 minutes — protection from replay attacks
  • HTTPS only — don't deliver to HTTP endpoints
  • Idempotent key in header (X-Webhook-ID) — receiver can deduplicate

Timeline for implementation: basic webhook endpoint with signature verification and queue — 1 day. Full system with retries, dead letter queue, delivery monitoring dashboard, subscription management — 2–3 days.