Treat every webhook as a claim until 5 checks pass. Anyone asking “what is a webhook?” needs that rule first. A webhook is an HTTP request another service sends to your URL the moment something happens, such as Stripe reporting a payment. Anyone can post to that URL, so check the signature, event id, age and sender’s record, then answer fast.

What is a webhook: the definition, and how one works

A webhook is an HTTP POST that one system sends to a URL you registered when a named event happens, such as a completed payment. Its life has 5 steps: you register the endpoint, the event occurs, the sender posts the event payload, your endpoint answers with a 2xx quickly, and the sender retries if it does not.

For an app that charges money, webhooks are how billing state reaches your database: the payment provider knows a card was charged or a subscription ended, and your tables only learn it when the event lands, which is the ground floor of SaaS billing process best practices. Stripe’s own description of the endpoint is short: after you register a webhook endpoint, Stripe “pushes real-time data to it when events happen in your Stripe account”, sent over HTTPS as a JSON payload.

What a webhook does, and how webhooks work from the first setting to the last retry, fits in five steps:

  1. 01 You register an endpoint URL with the sender and select the event types it should deliver. Stripe requires a publicly accessible HTTPS URL.
  2. 02 The event happens on the sender's side: a checkout completes, a user signs up, a row changes.
  3. 03 The sender makes an HTTP POST to your URL with the event data, usually as JSON, and an event id and event type in the body (Stripe) or in headers (GitHub), plus a signature header when a signing secret is set.
  4. 04 Your endpoint returns a 2xx status quickly, before any complex logic that could cause a timeout.
  5. 05 If no 2xx comes back, the sender tries again later, on its own schedule.

The signature header in step 3 depends on the sender: GitHub sends X-Hub-Signature-256 only if the webhook is configured with a secret. Step 5 has a long tail: Stripe can retry failed live webhook deliveries for up to three days and does not guarantee event order. The detail on registration, the quick 2xx and retries is in Stripe’s webhooks guide.

Four terms come up around webhooks. A webhook request is that HTTP POST itself: a method, some headers and a JSON body. The webhook URL, also called the endpoint, is the address you register, such as https://yourapp.com/api/webhooks/stripe. What a webhook is used for, in one line, is learning that something happened in another system without asking it over and over. A webhook integration is two products connected this way, one sending and one listening.

One event, trimmed and with fake ids, looks like this:

{
  "id": "evt_123",
  "object": "event",
  "type": "checkout.session.completed",
  "created": 1700000000,
  "livemode": true,
  "data": {
    "object": { "id": "cs_123", "object": "checkout.session" }
  }
}

The field names are the ones Stripe’s Event object uses; other senders name and nest theirs differently. The practical answer to how you create a webhook, registering an endpoint and firing test events at it, is the first part of learning how to test webhooks before real money flows through them.

What is a webhook subscription

A webhook subscription is the registration that tells a sender which event types to deliver and to which URL. Loosely, people also use it for the events a billing provider sends about a customer’s subscription. Either way, subscribe only to the event types your handler has a branch for.

Shopify uses the term in the strict sense. In Shopify’s WebhookSubscription reference: “A webhook subscription is a persisted data object created by an app using the REST Admin API or GraphQL Admin API. It describes the topic that the app wants to receive, and a destination where Shopify should send webhooks of the specified topic.”

The loose sense shows up with billing. Stripe sends customer.subscription.updated whenever a subscription changes and customer.subscription.deleted whenever a customer’s subscription ends. Stripe’s advice on which events to take is to configure endpoints to receive only the event types your integration requires, because listening for extra events, or all of them, puts undue strain on your server, and Stripe does not recommend it. The reverse mistake matters too: an endpoint that was never subscribed to the event it needs is the first thing to rule out when a payment goes through and nothing in your database changes.

What it means in practice for a small SaaS: webhook examples from the stack you already run

Webhook examples a small SaaS already receives come from the services it runs on: the payment provider reporting a completed checkout or a failed invoice, the auth provider reporting a new user, the email provider reporting a bounce, GitHub reporting a push, and the database reporting a changed row.

In the table, the event names are spelled the way each vendor spells them. The last two columns are my reading of a typical small SaaS, not the vendors’ words.

SenderEvent nameWhat your app does with itWhat goes wrong if it is missed
Stripecheckout.session.completed, invoice.payment_failedGrants the plan; starts the failed-payment pathA customer pays and stays on the free plan; a failed card keeps paid access
Clerkuser.createdCopies the new user into your own users tableRows that point at a user who is not in your table
Resendemail.bounced, email.complainedStops sending to that addressRepeated mail to dead or annoyed inboxes
GitHubpushStarts a deploymentCode merged and never shipped
SupabaseDatabase Webhooks on INSERT, UPDATE or DELETETells another system a row changedThe other system works from old data
Your app (sending)Your own event names, posted to a customer’s URL or a Slack or Discord incoming webhook URLTells a customer or a channel that something happenedThe customer’s system never hears about the change

The vendor definitions behind those rows are short. In Stripe’s event types, checkout.session.completed occurs when a Checkout Session has been successfully completed, and invoice.payment_failed whenever an invoice payment attempt fails. Resend’s webhook event types define email.bounced as the recipient’s mail server permanently rejecting the email, and email.complained as an email that was delivered and then marked as spam. Clerk’s docs show user.created in a sample payload and say Clerk uses Svix to send its webhooks. GitHub’s webhook events and payloads say each delivery carries an X-GitHub-Event header naming the event and an X-GitHub-Delivery header holding a GUID for it. Supabase Database Webhooks fire after a database row is changed and are a convenience wrapper around triggers using the pg_net extension. Slack’s incoming webhook gives you a unique URL to which you send a JSON payload with the message text, and Discord describes its webhooks as a low-effort way to post messages to channels.

The last row turns the arrow around, and that changes your job. Once a customer’s system registers a URL with your app to hear that an order shipped, your app owes that customer what Stripe owes you: an event id, a signature they can verify, and retries when their endpoint is down. Standard Webhooks, which calls itself “a set of open source tools and guidelines to send webhooks easily, securely and reliably”, defines one shared format for that: a webhook-id header that stays the same on every retry, a webhook-timestamp header and a webhook-signature header, signed with HMAC-SHA256 or ed25519. The Standard Webhooks specification is short enough to read before you write a sender. On the receiving side, a paid plan is granted by the webhook handler and never by the success page, a rule at the center of how to integrate Stripe safely.

What it is not: API vs webhook, and what Webhook.site is

API vs webhook comes down to who starts it. With an API call, your app pulls: it asks when it chooses. With a webhook, the other system pushes: it tells your app when something happens. Both run over HTTP, so a webhook is an API call in reverse, and the two work together: the webhook signals, an API call confirms.

Neither is better on its own; each fits a different moment. The table is my reading of the two patterns, apart from the Stripe cell, which comes from Stripe’s guide.

API callWebhook
Who starts itYour appThe other system
When it happensWhen your code decides to askWhen the event happens
DirectionYour app to the provider, answer comes backThe provider to your app, a status code goes back
What you must runNothing public; a client that sends requestsA public HTTPS endpoint that is always up
How a failure showsAn error in your own code, right awayIn the sender’s delivery log; Stripe lists each delivery and the next retry in the Event deliveries tab

A webhook is part of an API, not a separate technology: it is an ordinary HTTP request sent the other way. The Standard Webhooks project says webhooks “are part of a service’s API, though you can think of them as a sort of a reverse API”, and calls them “a common name for HTTP callbacks”. So the webhook and API difference is direction and timing, not protocol. The phrase “webhook API” usually means something narrower: the sender’s normal API for creating and listing webhook endpoints. Stripe, for example, lets you register an endpoint through the API or the Webhooks tab in Workbench. When a sender offers no webhooks at all, the fallback is polling: calling its API on a schedule and comparing what comes back.

What is webhook site, and what not to send it

Webhook.site is a request-inspection service: it gives you a unique URL and shows every request sent to it, which helps you see a payload’s shape before writing a handler. On the free version anyone who knows the URL’s ID can read what arrives, so it is for test events only.

Webhook.site’s FAQ says visitors “instantly get a free, unique, random URL and e-mail address”, and everything sent to them is shown instantly. Because the free version operates without a login, its data “is accessible to anyone who knows the ID of the URL”. Free URLs expire after 7 days and accept a max of 100 requests; URLs on a paid subscription are protected with login by default.

Picture a founder who wants to see what Stripe sends before writing a handler, so they paste a free Webhook.site URL into the live account as a second endpoint beside the real one, which Stripe allows. The extra endpoint is never removed, so each live checkout event, which can carry the customer’s name, email and address from the completed Checkout Session, is copied to a page that anyone who knows the URL’s ID can read, until the free URL expires or hits its request cap. Nothing failed, which is why nobody notices: the free URL did exactly what Webhook.site’s FAQ says it does.

The lesson I take from it: a public inspection URL is for test events, and anything a live customer typed stays with the endpoint you own and the sender’s own tools. The sender’s tools do the same job with no third party in the middle. Stripe’s CLI forwards events to a local listener with stripe listen --forward-to localhost:4242/webhook, so you can watch event payloads arrive on your own machine. Testing the handler itself, beyond watching payloads, is a separate job.

What has to be true before a webhook may change your data

A webhook may change your data only after 5 gates pass: the signature verifies against the raw body, the event id has not been processed before, the event is not older than the state it would overwrite, the object’s current state is confirmed with the sender’s API, and the 2xx goes back fast with the work done after.

The order of the gates and the “failure it prevents” column are my working rule. The “sender’s docs” column is Stripe’s documentation, as each row notes.

GateThe failure it preventsWhat the sender’s docs sayWhere the how-to lives
1. AuthenticA forged “payment succeeded” posted to your public URLStripe: Webhook signing secrets are per endpoint and separate from API keys, so the same URL has different test and live secrets.webhooks security and, for Stripe’s errors, Stripe webhook verification
2. Not a duplicateA retried delivery grants the plan twiceStripe: endpoints might occasionally receive the same event more than once; log the event IDs you have processed and skip thosehow to make a webhook handler idempotent
3. Not staleAn older event overwrites newer stateStripe: no order guarantee (the retry line under the steps above), and do not use created to determine event orderThe article in gate 5, its section on events delivered out of order
4. ConfirmedActing on a payload that is already out of dateStripe: a snapshot event’s data can be stale by the time you process it; fetch the latest version of the resource from the APIThis page
5. Acknowledged fast, processed safelyA slow handler makes the sender time out and retry, and a 200 sent before a failed write hides the failureStripe: the quick 2xx from step 4 above; process incoming events with an asynchronous queueStripe webhooks failing or not updating the database

Gate 4 is the one with no other home. Its Stripe cell comes from Stripe’s event destinations page, which also describes the other kind of event: a thin event includes only limited information, and you make a follow-up API call to fetch the complete object. My working rule follows from both: for anything that grants access or moves money, read the object’s current state from the sender’s API and act on that, not on the copy in the payload.

Two of my own audit numbers bear on these gates. Three of the audited apps had a real secret permanently in git history: a webhook signing secret, a live AI-provider key, and a Stripe test key with its webhook secret. A signature check proves little once its secret is readable in the repository. Only 1 of the 21 third-party apps, a healthcare FHIR hub, was credited with a real test suite, and even it skipped sign-up, login and the payment webhook. Both numbers come from 21 third-party apps I audited in June and July 2026: 11 public vibe-coded apps audited exhaustively across all 12 pillars, and a held-out set of 10 audited blind. They are a selected set of apps, not a random sample and not a rate for AI-built apps in general.

For a payments app, gates 1 and 2 together are one go-live condition, set against Stripe’s own go-live list, in what must be true before you take the first card.

How it shows up in a hardening sprint

In the Production Hardening Sprint, the first two gates are deliverables 5.1 and 5.2. Deliverable 5.1, webhook signature verification: we verify the payment provider’s signatures before processing webhook payloads, and we verify the work by confirming valid events succeed and invalid or altered payloads are rejected. Deliverable 5.2, idempotent webhook handling: we make every webhook handler safe to repeat, including concurrent delivery and its downstream side effects, then replay and concurrently deliver events and confirm one intended business effect. Building new product features or modules, completing unfinished core features or business workflows, and rebuilding core functionality that does not yet perform its intended job sit outside the sprint. Hosting, paid tools, and API usage remain in your accounts. Every deliverable’s wording, with its check, is in the published scope.

Common questions about webhooks

What replaced webhooks?

Nothing has replaced webhooks for server-to-server event notifications; the alternatives sit beside them. The usual ones are polling the sender’s API on a schedule, an event bus the sender pushes into, and, for live updates inside a browser, a WebSocket. Stripe’s event destinations, for example, include Amazon EventBridge and Azure Event Grid as well as webhook endpoints. In my reading, polling trades delay and wasted calls for simplicity, an event bus trades setup for scale, and a WebSocket solves a different problem: keeping a page up to date while a person is looking at it.

Can I use REST APIs with webhooks?

Yes. A typical integration uses the sender’s REST API in three places around its webhooks. You register and manage endpoints through the API (Stripe also offers the Webhooks tab in Workbench). You fetch the object’s current state when an event arrives, so you act on fresh data. And after an outage you can list past events: Stripe’s events endpoint goes back up to 30 days, and its Dashboard can resend a specific event for up to 15 days after it was created.

Is a webhook like a websocket?

No. RFC 6455, the WebSocket standard, describes a protocol that enables two-way communication between a client and a remote host that has opted in to it. In my reading, that means one connection held open so either side can speak at any time. A webhook is server-to-server, in the Standard Webhooks project’s words, and it is one HTTP request per event, sent one way.

What are the different types of webhooks?

Webhooks split three ways: by direction, by payload and by format. Direction means the ones your app receives versus the ones it sends. On payload, Stripe sends snapshot events, with the full object, and thin events, a lightweight notice you follow with an API call. On format, Standard Webhooks notes that every provider implements webhooks differently, and it offers one shared set of headers and signatures instead.