AI Tool Pipelines — Automate Your WorkflowsAI Tool Pipelines

Webhook Automation: A Practical Guide for API Workflows

7 min read · Updated Mar 30, 2026

Webhook event flow diagram showing real-time data delivery

A webhook is just an HTTP POST that another service sends you when something happens. That is the easy part. The hard part is everything that follows: verifying the request is real, surviving duplicate deliveries, returning a 200 in under one second, and not losing a payment event when your worker crashes at 4 a.m. This guide is the production checklist I wish I had read in 2019, with code you can copy and the four mistakes that have caused every webhook outage I have ever debugged.

Key takeaways

  • Webhooks beat polling on latency and cost. The break-even sits around one poll every 5 minutes.
  • Verify the signature on every request. Stripe, GitHub, and Shopify all sign with HMAC-SHA256 — reject anything unsigned.
  • Acknowledge fast (under 1 second), process slow. Push the work onto a queue and return 200 OK immediately.
  • Idempotency is non-negotiable. Webhook providers retry, so the same event-id will arrive twice. Deduplicate on the id.
  • Stripe’s webhook docs (2025) recommend a 5-second timeout for the receiver. Slower than that and they assume failure and retry.

Polling vs webhooks: when each one wins

Polling means your code asks every N seconds, "anything new?" Webhooks invert that — the source service tells you. Webhooks win on latency and on cost when events are rare. Polling wins when you do not control the source service, or when events are so frequent (more than one per second per consumer) that webhook overhead becomes its own problem. The crossover in my experience is roughly: if you would poll more than once every 5 minutes, switch to a webhook; if you would poll less often than once an hour, polling is simpler and you should not bother.

Polling vs webhooks at a glance. Costs assume AWS Lambda pricing as of June 2026.
PropertyPollingWebhooks
LatencyUp to the poll interval (often minutes)Sub-second
Cost at 1 event/dayHigh (constant requests, no event)Low (one request)
Cost at 100 events/secPredictableSpiky, queue back-pressure needed
Failure visibilityYou notice (your job is running)Silent unless monitored
ComplexityLow — cron + scriptHigher — signing, queues, retries

A minimal Express receiver — do not deploy this yet

typescript
import express from "express";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/webhooks/stripe", (req, res) => {
  const event = JSON.parse(req.body.toString());
  console.log("Got event", event.type, event.id);
  res.status(200).send("ok");
});

app.listen(3000, () => console.log("Listening on :3000"));

This handler will work for about a week before something goes wrong. It does not verify the signature, so anyone with the URL can fake events. It does the work inline, so a slow database call will time out the webhook. And it has no record of which event ids it has already seen, so a Stripe retry will double-charge your business logic. The next sections fix all three.

Step one: verify the signature

Every reputable webhook provider signs the payload. Stripe sends a Stripe-Signature header, GitHub sends X-Hub-Signature-256, Shopify sends X-Shopify-Hmac-SHA256. The algorithm is always HMAC-SHA256 over the raw request body using a shared secret. Verifying takes three lines of code and stops 100% of spoofed requests.

typescript
import crypto from "node:crypto";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

app.post("/webhooks/stripe", (req, res) => {
  const signature = req.headers["stripe-signature"] as string;
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      signature,
      endpointSecret,
    );
  } catch (err) {
    console.warn("Signature verification failed", err);
    return res.status(400).send("Invalid signature");
  }

  // event is now trusted
  res.status(200).send("ok");
});

Step two: acknowledge fast, process slow

Stripe times out at 5 seconds. GitHub at 10. Shopify at 5. If you do real work inside the handler — a database write, an email send, an API call — you will eventually breach the timeout, Stripe will assume failure, and they will retry. Retries on a slow handler stack up like a queue full of bricks. The fix is always the same: drop the event on a queue and return 200 OK immediately.

typescript
import { Queue } from "bullmq";

const webhookQueue = new Queue("stripe-events", {
  connection: { host: "localhost", port: 6379 },
});

app.post("/webhooks/stripe", async (req, res) => {
  const event = verify(req); // verify + parse, throws on invalid

  await webhookQueue.add(
    event.type,
    { event },
    {
      jobId: event.id, // dedupe key — BullMQ drops duplicates by id
      attempts: 5,
      backoff: { type: "exponential", delay: 1000 },
      removeOnComplete: 1000,
    },
  );

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

A separate worker process pulls from the queue and does the real work. If the worker crashes, BullMQ retries with exponential backoff. If Stripe sends the same event twice (it will, eventually), the second add() is dropped because the jobId matches. The webhook endpoint itself returns in under 50ms.

Step three: idempotency you can prove

Queue-level dedupe handles in-flight duplicates. For the cases where the worker successfully processed an event and then the same event arrives again three days later (network blip on the provider side, replay from their dashboard), you need a database-level check. The pattern is a table that records every processed event id with a unique constraint.

sql
CREATE TABLE webhook_events (
  event_id    TEXT PRIMARY KEY,
  provider    TEXT NOT NULL,
  event_type  TEXT NOT NULL,
  payload     JSONB NOT NULL,
  processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- In the worker, before doing the real work:
-- INSERT INTO webhook_events (event_id, provider, event_type, payload)
--   VALUES ($1, $2, $3, $4)
--   ON CONFLICT (event_id) DO NOTHING
--   RETURNING event_id;
-- If RETURNING is empty, you have already processed this event — skip.

The story I tell every team that wants to skip queues

November 2023, a Tuesday night around 9 p.m. A four-person e-commerce team I was advising launched a Black Friday promo on a Shopify store. Orders started arriving at 40 per minute. Their order-confirmation pipeline ran inline inside the Shopify webhook handler — verify signature, look up customer, send email via SendGrid, write to a Google Sheet. The Google Sheets API rate-limited at 60 writes per minute. By 9:14 p.m. the webhook handler was taking 8 seconds to respond. Shopify started retrying at the 5-second mark. The duplicate retries pushed the rate limiter into a full 60-second lockout. Every order webhook now took 13 seconds. Shopify retried again. By 9:30 p.m. the queue was 400 orders deep and growing. We fixed it at 11 p.m. by ripping the Sheets write out of the handler, dropping events onto a Redis queue, and processing them at the rate the Sheets API allowed. The lesson: a webhook handler is not allowed to do real work. It exists to say "I got it" and move on.

No-code path: webhooks in n8n

If writing an Express server and a worker is more than you need, n8n’s Webhook trigger gives you the same primitives in a visual editor. It supports signature verification (HMAC node), built-in queueing on the n8n queue-mode worker pool, and a Wait node for async processing. For prototypes and internal workflows the no-code path ships in 20 minutes. For customer-facing production payments, write the code. See the deeper walk-through in The Ultimate API Pipeline Stack for 2026.

Observability: the part you forget until 4 a.m.

  • Log every webhook with provider, event id, event type, signature-valid flag, and processing latency. Index on event id for forensics.
  • Alert on queue depth (more than 50 events backed up = something is wrong) and on failed-signature rate (sudden spike = someone is probing your endpoint).
  • Use Hookdeck or Svix in production if you cannot afford to build the queue + retry layer yourself. They handle signature verification, retries, and replay for a flat fee per million events.
  • Use ngrok or Hookdeck CLI in development to receive real provider webhooks against localhost. Faking payloads with curl will not catch signature bugs.
“A webhook receiver has one job: take the brick, put it on the conveyor belt, say thank you. Everything else happens downstream.”

Frequently asked questions

Frequently asked questions

What is the difference between a webhook and an API?

An API is a request-response interface you call when you want data. A webhook is the opposite — a provider calls your URL with an HTTP POST when an event happens. APIs are pull; webhooks are push.

How do I test a webhook locally?

Use ngrok or the Hookdeck CLI to expose your local server with a public URL, register that URL with the provider (Stripe, GitHub, Shopify dashboards all support custom URLs for test mode), and trigger real events. Faking payloads with curl skips signature verification, which is where most bugs live.

What status code should a webhook endpoint return?

Return 200 (or 2xx) as soon as you have accepted the event — do not wait for the work to finish. Return 4xx if the signature is invalid or the payload is malformed; the provider will not retry. Return 5xx if you genuinely failed and want a retry.

How do I prevent duplicate webhook events from being processed twice?

Two layers: dedupe at the queue level using the provider’s event id as the job id, then dedupe at the database level using a unique-constraint table of processed event ids. Both are needed because the queue dedupes within a TTL window and the database dedupes forever.

How long should a webhook take to respond?

Under one second is the safe target. Stripe and Shopify time out at 5 seconds, GitHub at 10. If your handler does more than verify-and-queue, you are too slow.

Should I use a managed service like Hookdeck or build my own webhook receiver?

Build your own if you have one or two providers and an engineer who likes Redis. Use Hookdeck or Svix when you have five or more sources, need to fan out to multiple destinations, or cannot lose an event during a deploy. The break-even is roughly $99 a month versus a half-day of engineering per quarter.