Introduction

Webhooks are HTTP callbacks that enable real-time event notifications. Instead of polling for changes, services push data to your endpoint when events occur. This guide covers webhook implementation and best practices.

What are Webhooks?

Webhooks are user-defined HTTP callbacks triggered by events:

Service A              Your Server
    β”‚                      β”‚
    β”‚  Event occurs        β”‚
    │─────────────────────>β”‚
    β”‚  POST /webhook       β”‚
    β”‚  { event: "..." }    β”‚
    β”‚                      β”‚
    β”‚  Process event       β”‚
    β”‚                      β”‚
    β”‚  200 OK              β”‚
    β”‚<─────────────────────│

Webhook vs Polling

Polling (inefficient):

// Check every 5 seconds
setInterval(async () => {
  const data = await fetch("/api/check");
  if (data.changed) {
    handleChange(data);
  }
}, 5000);

Webhook (efficient):

// Receive instant notification
app.post("/webhook", (req, res) => {
  handleEvent(req.body);
  res.status(200).send("OK");
});

Webhook Structure

Typical Webhook Payload

{
  "event": "user.created",
  "timestamp": "2024-12-07T10:00:00Z",
  "data": {
    "id": 123,
    "email": "user@example.com",
    "name": "Alice"
  }
}

Webhook Headers

POST /webhook HTTP/1.1
Host: your-server.com
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-Event: user.created
User-Agent: WebhookService/1.0

Security

1. Signature Verification

Provider signs payload:

const crypto = require("crypto");

function verifySignature(payload, signature, secret) {
  const hash = crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(payload))
    .digest("hex");

  return `sha256=${hash}` === signature;
}

app.post("/webhook", (req, res) => {
  const signature = req.headers["x-webhook-signature"];
  const secret = process.env.WEBHOOK_SECRET;

  if (!verifySignature(req.body, signature, secret)) {
    return res.status(401).send("Invalid signature");
  }

  // Process webhook
  handleWebhook(req.body);
  res.status(200).send("OK");
});

2. HTTPS Only

// ❌ Never accept webhooks over HTTP in production
if (req.protocol !== "https") {
  return res.status(400).send("HTTPS required");
}

3. IP Whitelisting

const ALLOWED_IPS = ["192.0.2.0", "203.0.113.0"];

app.post("/webhook", (req, res) => {
  const clientIP = req.ip;
  if (!ALLOWED_IPS.includes(clientIP)) {
    return res.status(403).send("IP not allowed");
  }
  // Process webhook
});

Implementation

Express.js Webhook Endpoint

app.post("/webhook", async (req, res) => {
  try {
    // Verify signature
    if (!verifySignature(req.body, req.headers["x-signature"])) {
      return res.status(401).send("Invalid signature");
    }

    // Process immediately
    res.status(200).send("OK");

    // Handle asynchronously
    await processWebhook(req.body);
  } catch (error) {
    console.error("Webhook error:", error);
    res.status(500).send("Error");
  }
});

Idempotency

const processedEvents = new Set();

app.post("/webhook", async (req, res) => {
  const eventId = req.headers["x-event-id"];

  // Check if already processed
  if (processedEvents.has(eventId)) {
    return res.status(200).send("Already processed");
  }

  // Process event
  await handleEvent(req.body);
  processedEvents.add(eventId);

  res.status(200).send("OK");
});

Best Practices

βœ… Do This

1. Respond quickly

// Acknowledge immediately
res.status(200).send("OK");

// Process asynchronously
setImmediate(() => processWebhook(data));

2. Implement retries

// Provider retries on non-2xx responses
if (processingFailed) {
  return res.status(500).send("Retry");
}

3. Log all webhooks

app.post("/webhook", (req, res) => {
  console.log("Webhook received:", {
    event: req.body.event,
    timestamp: new Date(),
    payload: req.body,
  });
  // Process...
});

❌ Don’t Do This

1. Don’t process synchronously

// ❌ Blocks response
app.post("/webhook", async (req, res) => {
  await longRunningProcess(req.body); // Takes 10 seconds
  res.status(200).send("OK");
});

// βœ… Acknowledge immediately
app.post("/webhook", (req, res) => {
  res.status(200).send("OK");
  processAsync(req.body);
});

2. Don’t trust webhook data

// ❌ No validation
app.post('/webhook', (req, res) => {
  await db.users.create(req.body.data); // Dangerous!
});

// βœ… Validate first
app.post('/webhook', (req, res) => {
  const validated = validateSchema(req.body);
  await db.users.create(validated);
});

Testing Webhooks

Use our tools:

Conclusion

Webhooks enable real-time integrations:

Key principles:

  • Verify signatures
  • Respond quickly
  • Process asynchronously
  • Handle retries
  • Log everything

Benefits:

  • Real-time updates
  • Efficient (no polling)
  • Event-driven architecture
  • Scalable

Next Steps