Skip to content

Webhook Flow

DodaTech 5 min read

title: "Webhook Flow" description: "Understand the complete webhook flow from event trigger to delivery, including registration, verification, delivery, acknowledgment, and retry." weight: 13 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "webhooks"]


The webhook flow describes the complete lifecycle of a webhook message, from event registration on the consumer side to successful delivery and acknowledgment.

## What You'll Learn

- Webhook registration process
- URL verification (challenge-response)
- Event triggering and payload creation
- Delivery and acknowledgment
- Retry and failure handling

## Why It Matters

Understanding the complete webhook flow helps you build reliable integrations. Each step has specific requirements and potential failure modes.

## Real-World Use

A payment processor's webhook flow includes: merchant registers webhook URL, processor sends verification challenge, merchant responds with challenge hash, and all subsequent payment events trigger signed webhook deliveries with automatic retries.

## Flow Chart

```mermaid
sequenceDiagram
    participant C as Consumer
    participant P as Provider
    
    C->>P: Register webhook URL
    P->>C: Verification challenge
    C->>P: Challenge response
    Note over C,P: Webhook verified
    Note over P: Event occurs
    P->>P: Create payload
    P->>P: Sign payload
    P->>C: POST /webhook
    C->>C: Verify signature
    C->>C: Process event
    C->>P: 200 OK
    Note over P: Delivery complete

Code Examples

Example 1: Webhook Registration with Verification

// Provider: webhook registration with challenge
app.post('/api/webhooks', async (req, res) => {
  const { url, events } = req.body;

  // Generate verification challenge
  const challenge = crypto.randomBytes(32).toString('hex');
  const webhookId = generateId();

  // Send verification request to consumer
  try {
    const verificationResult = await axios.post(url, {
      type: 'verification',
      webhookId,
      challenge,
    }, {
      headers: { 'Content-Type': 'application/json' },
      timeout: 10000,
    });

    // Verify consumer responded with correct challenge
    if (verificationResult.data.challenge === challenge) {
      // Webhook verified successfully
      saveWebhook(webhookId, url, events);
      res.status(201).json({
        id: webhookId,
        status: 'verified',
        secret: generateSecret(),
      });
    } else {
      res.status(400).json({
        error: 'Verification failed: challenge mismatch',
      });
    }
  } catch (error) {
    res.status(400).json({
      error: 'Verification failed: cannot reach consumer URL',
    });
  }
});

// Consumer: handle verification
app.post('/webhooks/verify', (req, res) => {
  const { type, webhookId, challenge } = req.body;

  if (type === 'verification') {
    // Respond with the challenge to verify
    res.json({ challenge });
  }
});

Expected output: Provider sends verification challenge to consumer URL, consumer responds with the challenge, and webhook is registered as verified.

Example 2: Complete Webhook Delivery

async function deliverWebhook(webhook, event) {
  const payload = {
    id: generateEventId(),
    type: event.type,
    created_at: new Date().toISOString(),
    data: event.data,
  };

  const payloadStr = JSON.stringify(payload);
  const signature = crypto
    .createHmac('sha256', webhook.secret)
    .update(payloadStr)
    .digest('hex');

  const delivery = {
    webhookId: webhook.id,
    eventId: payload.id,
    url: webhook.url,
    payload: payloadStr,
    signature,
    attempt: 1,
    maxAttempts: 5,
    status: 'pending',
  };

  for (let attempt = 1; attempt <= delivery.maxAttempts; attempt++) {
    delivery.attempt = attempt;
    
    try {
      const response = await axios.post(webhook.url, payload, {
        headers: {
          'Content-Type': 'application/json',
          'X-Webhook-ID': webhook.id,
          'X-Event-ID': payload.id,
          'X-Signature-256': signature,
          'X-Delivery-Attempt': attempt,
        },
        timeout: 15000,
      });

      if (response.status >= 200 && response.status < 300) {
        delivery.status = 'delivered';
        logDelivery(delivery);
        return { success: true, delivery };
      }
    } catch (error) {
      delivery.lastError = error.message;
      delivery.status = 'failed';
      logDelivery(delivery);

      if (attempt < delivery.maxAttempts) {
        // Exponential backoff
        await sleep(Math.min(1000 * Math.pow(2, attempt - 1), 60000));
      }
    }
  }

  delivery.status = 'permanently_failed';
  logDelivery(delivery);
  return { success: false, delivery };
}

function logDelivery(delivery) {
  console.log(
    `Delivery ${delivery.eventId} to ${delivery.url}: ` +
    `${delivery.status} (attempt ${delivery.attempt})`
  );
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Expected output: Full webhook delivery with signature, retry logic with exponential backoff, and delivery logging.

Example 3: Consumer Webhook Processing Pipeline

from flask import Flask, request, jsonify
import hashlib
import hmac
import threading
from queue import Queue

app = Flask(__name__)
webhook_queue = Queue()

@app.route('/webhooks/handler', methods=['POST'])
def handle_webhook():
    # Verify signature
    signature = request.headers.get('X-Signature-256')
    payload = request.get_data()
    
    if not verify_signature(payload, signature):
        return jsonify({"error": "Invalid signature"}), 401

    # Acknowledge immediately
    event_id = request.headers.get('X-Event-ID')
    
    # Queue for async processing
    webhook_queue.put({
        'event_id': event_id,
        'type': request.json.get('type'),
        'data': request.json.get('data'),
        'headers': dict(request.headers),
    })

    return jsonify({"status": "accepted", "event_id": event_id}), 200

def verify_signature(payload, signature):
    if not signature:
        return False
    
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(expected, signature)

def webhook_worker():
    while True:
        webhook = webhook_queue.get()
        try:
            process_event(webhook['type'], webhook['data'])
        except Exception as e:
            log_error(f"Failed to process {webhook['event_id']}: {e}")
        finally:
            webhook_queue.task_done()

# Start worker thread
threading.Thread(target=webhook_worker, daemon=True).start()

if __name__ == '__main__':
    app.run(port=8080)

Expected output: Consumer validates signature, acknowledges immediately, and processes the webhook asynchronously.

Common Mistakes

Mistake Explanation
Processing synchronously Long processing delays acknowledgment and may cause provider timeouts
Skipping URL verification Without verification, webhooks may be sent to incorrect or malicious URLs
Not tracking delivery attempts Without logging, you cannot debug delivery failures or measure reliability
Ignoring webhook IDempotency Duplicate deliveries cause data corruption without idempotent handling
Not having a backup consumer Single consumer URLs are a single point of failure for critical webhooks

Practice Questions

  1. What is the purpose of webhook URL verification?
  2. How does the provider handle delivery failures?
  3. Why should consumers acknowledge webhooks immediately?
  4. What information should be in webhook headers?
  5. How do you handle duplicate webhook deliveries?

Challenge

Build a complete webhook system with registration, URL verification (challenge-response), signed delivery with retry logic, and a consumer pipeline that acknowledges, validates, and processes events asynchronously.

FAQ

What happens if the consumer does not verify the webhook URL?

The provider will not save the webhook registration. Verification ensures the URL is valid and under the consumer's control.

How long should consumers take to acknowledge?

Acknowledge within 5-10 seconds. Providers typically timeout after 15-30 seconds.

What is the difference between HTTP 200 and 202 for webhooks?

200 means processed, 202 means accepted for processing. Both stop retries. Use 202 for async processing.

How do providers handle consumer rate limiting?

Providers should respect Retry-After headers and implement adaptive delivery rates based on consumer responses.

Can a consumer reject a webhook after accepting it?

No, returning 2xx stops retries. If processing fails later, the consumer should handle it internally.

What is the expected response format for webhooks?

A simple JSON {'status': 'ok'} or empty body with 200 status is standard. Avoid large response bodies.

Mini Project

Build a webhook flow simulator that demonstrates the complete lifecycle: registration, verification, event triggering, signed delivery, acknowledgment, retry on failure, and delivery logging with a web dashboard.

What's Next

Learn about webhook payload formats

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro