Integration & APIs/
Lesson

Webhooks are the standard for real-time communication between services. Every time you see "real-time notifications" or "instant updates" in a product, there is probably a webhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. behind it. But webhooks are tricky, many integrations work fine in development and fall apart in production on edge cases that developers never considered.

WebhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. vs APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. pollingWhat is polling?Repeatedly asking a server at regular intervals if anything has changed, which works but wastes resources when nothing is new.

There are two ways to learn about events in another system: you can ask repeatedly (polling) or you can be told when something happens (webhooks). The difference matters for cost, latencyWhat is latency?The time delay between sending a request and receiving the first byte of the response, usually measured in milliseconds., and reliability.

Polling (the naive approach):

// Your server asks Stripe every minute: "Any new orders?"
setInterval(async () => {
  const orders = await fetch('https://api.stripe.com/v1/orders?status=new');
  for (const order of orders.data) {
    await processOrder(order);
  }
}, 60000);

Problems with polling: up to 60 seconds of latency, unnecessary API calls when nothing changed, you burn through rate limits, and you pay for compute to run the polling loop 24/7.

Webhook (the event-driven approach):

// Stripe calls YOUR URL the instant an order is created
app.post('/webhooks/stripe', (req, res) => {
  const event = req.body;
  // Process immediately - no delay, no wasted calls
  handleEvent(event);
  res.status(200).send('OK');
});

ApproachLatencyAPI callsCostComplexity
Polling every 60sUp to 60s delay1,440/day (even if 0 events)High (wasted compute + API calls)Low
Polling every 5sUp to 5s delay17,280/dayVery highLow
WebhookNear-instantOnly when events happenLowMedium (signature, idempotency)

The trade-off is clear: webhooks are cheaper and faster, but they require you to handle security (signature verification), reliability (idempotency), and infrastructure (a publicly accessible endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.).

AI pitfall
Ask AI to "integrate with the Stripe API to get notified about payments" and it will sometimes generate a polling loop instead of a webhook handler. AI defaults to the "pull" pattern because it is conceptually simpler. Always specify "push-based webhook handler" or "webhook endpoint" in your prompt to get the right pattern.
02

WebhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. lifecycle

Here is what happens when an event triggers a webhook:

1. Event occurs at Stripe (e.g., payment succeeds)
2. Stripe creates an event object with a unique ID
3. Stripe POSTs the event to your registered URL
4. Your server receives the request
5. You verify the signature
6. You respond 200 (acknowledgment)
7. You process the event asynchronously

The critical insight is that steps 5-6 should happen in milliseconds. Step 7 (the actual business logic) can take as long as it needs because you have already acknowledged receipt.

What can go wrong

Failure scenarioSender behaviorYour responsibility
Server downRetries with exponential backoff (hours to days)Ensure idempotent processing when you recover
Response too slow (>10s)Times out, treats as failure, retriesRespond 200 immediately, process async
Error response (4xx/5xx)Retries (except permanent 4xx in some providers)Fix the handler, not the response code
Unstable networkRequest lost or duplicatedDeduplicate using event IDs
Payload too largeMay truncate or failCheck content-length limits on your server

Each of these scenarios happens in production. Not "might happen", happens. If you handle 1,000 webhooks per day, you will see retries within the first week. If you handle 10,000 per day, you will see every failure mode on this list within a month.

03

WebhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. signature

Good to know
Signature verification is the most commonly skipped step in webhook implementations. Without it, anyone who discovers your webhook URL can send fake payment events. This is not a theoretical risk, security researchers actively scan for unverified webhook endpoints, and attackers use them to trigger fake order fulfillments.

Why verify? Your webhook URL is just a public HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.. Anyone on the internet can POST to it. How do you know the request actually came from Stripe and not from an attacker?

How signatures work:

  1. You and Stripe share a secret key (the webhook signing secret)
  2. When Stripe sends a webhook, it computes an HMACWhat is hmac?A keyed hash function that produces a fixed-length digest from a message and a secret key - used in webhook signatures and JWT signing.-SHA256 of the payloadWhat is payload?The data sent in the body of an HTTP request, such as the JSON object you include when creating a resource through a POST request. using that secret
  3. Stripe includes the signature in the Stripe-Signature header
  4. You recompute the HMAC on your side and compare, if they match, the request is genuine

const stripe = require('stripe')(process.env.STRIPE_SECRET);

app.post('/webhooks/stripe', (req, res) => {
  const sig = req.headers['stripe-signature'];
  const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

  let event;
  try {
    // Signature verification - this is the critical line
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log(`Webhook signature verification failed.`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // If we reach here, the event is authentic
  handleEvent(event);
  res.status(200).json({ received: true });
});

Different providers use different signature schemes. Stripe uses HMAC-SHA256 in a custom header. GitHub uses HMAC-SHA256 in X-Hub-Signature-256. Twilio uses a different algorithm entirely. Always check the providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually.'s documentation for the exact verification method.

ProviderSignature headerAlgorithmRaw body required?
StripeStripe-SignatureHMAC-SHA256 + timestampYes
GitHubX-Hub-Signature-256HMAC-SHA256Yes
TwilioX-Twilio-SignatureHMAC-SHA1 + URLNo (uses form params)
ShopifyX-Shopify-Hmac-SHA256HMAC-SHA256 (base64)Yes
SlackX-Slack-SignatureHMAC-SHA256 + timestampYes
Edge case
Stripe's signature includes a timestamp to prevent replay attacks. If your server clock is significantly off (more than 5 minutes), signature verification will fail even for legitimate webhooks. Make sure your servers use NTP for time synchronization.
04

Idempotency: the key concept

Problem: The same webhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. can arrive multiple times. This happens because of retries after timeouts, network duplicates, or sender bugs. If your handler creates an order on every call, duplicate webhooks create duplicate orders.

Solution: Use the event's unique ID as an idempotency keyWhat is idempotency key?A unique client-generated string sent with a mutation request so the server can safely deduplicate retried requests.. Before processing, check if you have already handled this event. If yes, skip it and return 200.

app.post('/webhooks/stripe', async (req, res) => {
  const eventId = req.body.id;  // Unique Stripe event ID (evt_...)

  // Check if already processed
  if (await isEventProcessed(eventId)) {
    console.log(`Event ${eventId} already processed, skipping`);
    return res.status(200).send('Already processed');
  }

  try {
    await processPayment(req.body);
    // Mark as processed (with TTL for cleanup)
    await markEventProcessed(eventId, { ttl: '7d' });
    res.status(200).send('OK');
  } catch (error) {
    // Do NOT mark as processed - let the sender retry
    console.error('Processing failed:', error);
    res.status(500).send('Error');
  }
});

The key detail is what happens on error: you return 500 without marking the event as processed. This tells the sender to retry, and the next attempt will find the event unprocessed and try again. Only mark an event as processed after successful handling.

05

Best practices

Respond quickly, process later

// Good: acknowledge immediately, process in the background
app.post('/webhook', async (req, res) => {
  if (!isValidSignature(req)) {
    return res.status(400).send('Invalid signature');
  }

  // Respond immediately - the sender is waiting
  res.status(200).send('Received');

  // Background processing (queue, worker, or async)
  await processWebhookAsync(req.body);
});
// Bad: blocking processing before responding
app.post('/webhook', async (req, res) => {
  await processPayment(req.body);     // Can take 5s!
  await sendConfirmationEmail();      // + 2s
  await updateInventory();            // + 1s
  res.status(200).send('OK');         // 8 seconds later - timeout risk
});

Retry behavior by status codeWhat is status code?A three-digit number in an HTTP response that tells the client what happened: 200 means success, 404 means not found, 500 means the server broke.

HTTP CodeStripe behaviorGitHub behaviorYour intent
200-299Success, no retrySuccess, no retry"I got it, all good"
4xx (except 429)No retryRetries for 3 days"Bad request, do not retry"
429Retry with backoffRetry with backoff"Too many requests, slow down"
5xxRetry up to 3 daysRetry for 3 days"Server error, please retry"

Event type routing

Always check the event type before processing. A single webhookWhat is webhook?An HTTP request that a service sends to your server when a specific event occurs, like a payment completing. Your server processes the event automatically. endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. typically receives many different event types, and handling the wrong type can cause data corruption.

switch (event.type) {
  case 'payment_intent.succeeded':
    await fulfillOrder(event.data.object);
    break;
  case 'payment_intent.payment_failed':
    await notifyCustomer(event.data.object);
    break;
  case 'customer.subscription.deleted':
    await revokeAccess(event.data.object);
    break;
  default:
    console.log(`Unhandled event type: ${event.type}`);
}

Log unhandled event types rather than ignoring them silently. This makes it easy to discover new events you should be handling when you expand your integration.