正在加载,请稍候…

What Is a Webhook? How Webhooks Work and How to Build Reliable Handlers

Learn what webhooks are, how they differ from polling, how to receive and verify webhook payloads securely, and how to handle retries and idempotency.

What Is a Webhook?

A webhook is an HTTP callback — another service sends a POST request to your URL when an event occurs. Instead of asking "did anything change?" repeatedly (polling), the service notifies you immediately.

Polling:   Your app → API every 60s: "New payments?"  API: "No... No... Yes!"
Webhook:   Payment happens → Stripe immediately POSTs to your-app.com/webhooks

Webhooks are real-time, efficient (no wasted requests), and simpler to scale than polling.

How Webhooks Work

  1. You register a URL with the service: https://your-app.com/webhooks/stripe
  2. Event occurs (payment, user signup, etc.)
  3. Service POSTs a JSON payload to your URL
  4. Your server processes the event and returns HTTP 200
  5. If you don't return 2xx, the service retries (exponential backoff)

Receiving Webhooks (Express)

// IMPORTANT: use express.raw() not express.json() — need raw body for signature verification
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  // Step 1: Verify signature FIRST
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      req.headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send('Signature verification failed');
  }

  // Step 2: Acknowledge immediately
  res.json({ received: true });

  // Step 3: Process asynchronously (after response)
  setImmediate(() => processEvent(event));
});

Verifying Signatures Manually

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  const sigBuffer = Buffer.from(signature, 'hex');
  const expectedBuffer = Buffer.from(expected, 'hex');
  if (sigBuffer.length !== expectedBuffer.length) return false;
  return crypto.timingSafeEqual(sigBuffer, expectedBuffer);  // prevent timing attacks
}

// GitHub: 'X-Hub-Signature-256' header is 'sha256=...'
app.post('/webhooks/github', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-hub-signature-256'].replace('sha256=', '');
  if (!verifyWebhook(req.body, sig, process.env.GITHUB_SECRET)) {
    return res.status(401).send('Unauthorized');
  }
  // process...
});

Handling Duplicate Events (Idempotency)

Services retry on failure — your handler may receive the same event multiple times. Always deduplicate:

async function processPayment(paymentIntent) {
  // Check if already processed
  const existing = await db.query(
    'SELECT id FROM orders WHERE payment_id = $1',
    [paymentIntent.id]
  );
  if (existing.rowCount > 0) return;  // Already handled, skip

  // Process for the first time
  await db.query('INSERT INTO orders ...', [paymentIntent.id, ...]);
}

Respond Quickly, Process Later

Webhook services timeout in 5-30 seconds. For slow processing, acknowledge first and queue the work:

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = verifyAndParse(req);
  await queue.add('process-webhook', { event });  // push to job queue
  res.json({ received: true });  // respond immediately
});

Testing Locally

# Expose localhost with ngrok
ngrok http 3000
# → https://abc123.ngrok.io/webhooks/stripe

# Stripe CLI: forward real events to local server
stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger payment_intent.succeeded

Signature Header Reference

Service Header Algorithm
Stripe Stripe-Signature HMAC-SHA256 + timestamp
GitHub X-Hub-Signature-256 HMAC-SHA256
Shopify X-Shopify-Hmac-Sha256 HMAC-SHA256 (base64)
Slack X-Slack-Signature HMAC-SHA256 + timestamp

→ Generate and verify HMAC signatures with the HMAC Generator.