Imagine you're building an e-commerce platform and using an external payment processor like Stripe to collect payments from users.
Once a user completes a payment, your system needs to:
But here's the challenge:Stripe operates on its own infrastructure. Your system doesn’t control it. So how do you know instantly when a payment goes through?
A naive solution would be to keep asking Stripe every few seconds:
This is known as polling.
Now imagine doing this for every order on a site like Amazon.
It just doesn’t scale and wastes server resources.
Instead of your app repeatedly asking, what if Stripe could just tell you when the payment succeeds?
That’s what webhooks do.
In this article, we will explore:
A webhook is a simple way for one system (provider) to notify another system (receiver) in real time when an event happens using an HTTP request.
Instead of one system asking repeatedly if something happened, the other system simply pushes the data as soon as the event is triggered.
Let’s say you go to a busy restaurant.
The host says: “There’s a 30-minute wait. Please leave your number, and we’ll text you when your table is ready.”
You don’t need to stand at the counter asking every 2 minutes: “Is my table ready yet?”
Instead, you walk away, and when your turn comes, they notify you automatically.
That’s the idea behind webhooks.
At a high level, webhooks work through registration, triggering, and delivery.
Let’s walk through a real example to see what actually happens under the hood.
Let’s say you’ve built a system that needs to react when someone opens a pull request (PR) in a GitHub repository — maybe to kick off a CI/CD pipeline, send a Slack notification, or run automated tests.
Here’s how the webhook flow works step-by-step:
https://myapp.com/github-webhook
)pull_request
, push
, or issue_comment
At this point, GitHub knows where to send data whenever those events occur.
POST
request to your webhook URL:200 OK
to acknowledge receiptTo integrate with webhooks successfully, you need to understand what’s actually being sent to your server when the event occurs.
Most webhook providers use the HTTP POST
method to send event data to your server.
GET
, PUT
, or even PATCH
, but POST
is the de facto standard for webhook delivery.The headers in a webhook request often include metadata and security-related information.
Common headers include:
The body of a webhook request typically contains event data in JSON format. This data includes what happened, when it happened, and which user/resource it’s related to.
Once you’ve registered your webhook URL with a provider like GitHub or Stripe, the next step is to build a robust endpoint on your server that can receive and process these events reliably.
This sounds simple — just receive a POST request, right?
But in production, things get tricky:
Let’s break down how to setup your webhook receiver the right way.
Start by exposing a simple HTTP endpoint like:
POST /webhook
This endpoint should:
Content-Type
)Webhook events can be retried, duplicated, or even replayed by providers. Your handler must ensure that processing the same event multiple times has no side effects.
How?
evt_1234
in Stripe, delivery_id
in GitHub)This ensures exactly-once processing, even if you receive the same event more than once.
200 OK
after successful handling400 Bad Request
if the payload is invalid or malformedWebhooks are public endpoints. That means anyone on the internet can POST to them. You must validate incoming requests.
Most providers include a cryptographic signature of the payload, using a shared secret. This ensures the payload came from the real source.
Example (Stripe):
whsec_abc123
This prevents spoofed or forged webhooks.
Some providers publish static IP ranges from which webhooks are sent. You can optionally block all other IPs to tighten access.
Downside: This adds DevOps complexity and can break things if IPs change.
As your application grows and starts receiving thousands or even millions of webhook events daily, a simple synchronous handler won’t be enough.
To make your webhook system reliable, fault-tolerant, and scalable, you need a design that’s asynchronous, decoupled, and observable.
Let’s walk through how to build a production-grade webhook processing pipeline.
Don’t do heavy processing inside your webhook endpoint.
Instead:
This way, your server can return a fast
200 OK
, while actual processing happens in the background.
To ensure traceability and recovery, store every incoming event in a database:
queued
, processed
, failed
)Use a durable data store like PostgreSQL, MongoDB, or DynamoDB.
This gives you the ability to replay failed events and debug issues.
Background workers (or worker pools) pull events from the queue and perform actual business logic:
Why use workers?
Sometimes, event processing fails temporarily due to a database issue, timeout, or an unavailable downstream API.
Your workers should retry those events automatically, using a backoff strategy:
Always limit retries and log failures after the final attempt.
If a webhook consistently fails after multiple retries, don’t keep retrying forever.
Instead, send it to a Dead Letter Queue, a special holding queue for problematic events.
From there, you can:
This prevents bad events from clogging your pipeline and ensures no event is lost silently.
You can’t fix what you can’t see. Add observability at every stage of the pipeline.
Track metrics like:
Set alerts for:
Popular observability tools:
With proper observability, you’ll catch issues early before they impact your users.