27.4. Webhooks
Table of Contents
Why Webhooks Exist
Most APIs use the request response style: your backend sends a request, the other service returns a response, and that is the end of the story.
Sometimes this model is not enough. Examples:
- A payment provider needs to tell you when a payment is confirmed.
- A file processing service must tell you when a video is transcoded.
- A Git hosting service must inform you when someone pushes new code.
You could poll their API repeatedly, but that is wasteful and slow. Webhooks solve this.
A webhook is a way for one system to call another system automatically when an event happens. Instead of you calling their API, they call yours.
In API terms:
- In normal REST APIs, you are the client.
- With webhooks, you are the server that receives HTTP requests from someone else.
Your backend exposes an HTTP endpoint, and another service sends POST requests to it whenever a specific event occurs.
Core idea: A webhook is an HTTP callback. Event happens, then the provider sends an HTTP request to your URL automatically.
Typical flow:
- You register a URL with the provider (for example,
https://api.example.com/webhooks/payment). - An event happens in the provider system (for example, payment succeeded).
- The provider creates a message describing the event.
- The provider performs an HTTP POST to your URL with that message.
- Your backend receives, validates, and processes the event, then returns an HTTP response.
Webhooks vs REST APIs
Webhooks and REST APIs complement each other. You will often use both in the same integration.
Direction of Communication
| Aspect | REST API | Webhook |
|---|---|---|
| Who initiates | Client (you) | Provider (external service) |
| Typical HTTP verb | GET, POST, PUT, PATCH, DELETE | Usually POST |
| When it happens | Whenever you decide | Whenever provider decides after an event |
| Common usage | Query or change data on demand | Receive notifications about asynchronous events |
Example:
- REST: You send
GET /payments/{id}to ask, "Is this payment done?" - Webhook: Provider sends
POST /webhooks/paymentsto tell you, "This payment just succeeded."
Push vs Pull
REST APIs are pull based. You pull information, usually by polling if you want to know about changes.
Webhooks are push based. Provider pushes information to you as soon as it changes.
This makes webhooks useful for:
- Real time or near real time updates.
- Avoiding heavy polling.
- Long running asynchronous operations, such as video processing or large imports.
When to Use Which
Use REST APIs when:
- Your system controls the timing.
- You need to read or modify data on demand.
- You need strong consistency at a specific moment: "Give me the current state now."
Use webhooks when:
- You want to be notified about events.
- The other system controls when events happen.
- You can tolerate eventual consistency: "Tell me when something changes."
In many designs:
- You use a webhook to know that "something changed".
- You use the provider REST API to fetch the full details or confirm the state.
For example:
- Webhook: "Order 123 status updated."
- Your system receives the webhook, then calls the provider REST API:
GET /orders/123to get the latest data.
Typical Webhook Architecture
Key Components
A simple webhook setup has these parts:
- Provider: External service that sends notifications.
- Webhook endpoint: Your HTTP endpoint that receives notifications.
- Verification layer: Checks that the request is really from the provider and not tampered with.
- Processing logic: Updates your database and performs business actions.
- Response logic: Returns the correct HTTP status code quickly.
- Error handling / retries: Deals with failures and duplicate deliveries.
High level diagram:
- Provider event occurs.
- Provider sends POST to your
/webhooks/...endpoint. - Your endpoint validates the request and authentication.
- Your code stores the event in a database or queue.
- A background worker processes it and updates your domain models.
- Your endpoint returns a simple HTTP 200 or 204 quickly.
Example JSON Payloads
Most webhooks use JSON. A provider will often include:
- An
idfor the event. - A
typestring describing what happened. - A
created_attimestamp. - A
dataobject with the actual resource.
Example: payment provider webhook:
{
"id": "evt_12345",
"type": "payment.succeeded",
"created_at": "2026-08-28T10:00:00Z",
"data": {
"payment_id": "pay_98765",
"amount": 4999,
"currency": "USD",
"order_id": "order_42",
"status": "succeeded"
}
}Example: Git hosting webhook for a push:
{
"id": "push_abc",
"type": "git.push",
"created_at": "2026-08-28T10:30:00Z",
"data": {
"repository": "backend-course",
"branch": "main",
"commit_id": "a1b2c3d4",
"pusher": "alice"
}
}Many providers also pass metadata in headers, for example:
- Event type:
X-Event-Type: payment.succeeded - Signature:
X-Signature: sha256=... - Delivery id:
X-Event-Id: evt_12345
You will use these headers to route and verify events.
Designing Webhook Endpoints
Endpoint URL Design
You can use different URL patterns. Pick one that is simple and clear.
Common approaches:
| Style | Example URL | Notes |
|---|---|---|
| Single endpoint | /webhooks | All providers and event types in one place. |
| Per provider | /webhooks/stripe, /webhooks/github | Clear ownership, separate auth logic possible. |
| Per domain area | /webhooks/payments, /webhooks/subscriptions | Route by business domain. |
For beginners, use per provider. It is straightforward and contains provider specific logic in one file.
Example design:
POST /webhooks/paymentsreceives payment provider events.POST /webhooks/gitreceives Git provider events.
HTTP Methods and Status Codes
Webhooks almost always use POST. Some rare cases may use PUT, but POST is the standard.
Your endpoint must return a meaningful HTTP status code:
| Status | Meaning for provider |
|---|---|
| 2xx | Event accepted successfully. |
| 4xx | Your endpoint thinks there is a permanent problem. |
| 5xx | Temporary server problem, provider may retry later. |
Simple guideline:
Always try to return 2xx if:
- The signature is valid.
- The payload is well formed.
- You have successfully stored or queued the event.
Return 4xx if: - Signature is invalid or missing.
- The payload format is wrong or missing required fields.
Return 5xx for unexpected internal errors.
Providers often retry automatically when they receive 5xx or sometimes when they receive network errors.
Example Minimal Endpoint Logic
Imagine a simple Python FastAPI endpoint (conceptual, not a full FastAPI lesson):
from fastapi import APIRouter, Request, HTTPException
router = APIRouter()
@router.post("/webhooks/payments")
async def payments_webhook(request: Request):
raw_body = await request.body()
signature = request.headers.get("X-Signature")
if not is_valid_signature(raw_body, signature):
raise HTTPException(status_code=400, detail="Invalid signature")
event = await request.json()
# Basic validation
if "id" not in event or "type" not in event:
raise HTTPException(status_code=400, detail="Invalid payload")
# Store event for later processing
save_event_to_db(event)
# Return quickly
return {"received": True}Key points:
- Verify first.
- Validate payload.
- Store and return a success response.
- Perform heavy work later.
Securing Webhooks
Security is one of the most important parts of webhook design. You cannot trust any incoming HTTP request automatically.
You must solve at least three problems:
- Is this request really from the provider?
- Has the body been tampered with?
- Can attackers replay old events?
Shared Secrets and Signatures
The most common solution uses a shared secret and a signature header.
Basic idea:
- You and the provider agree on a secret string, for example
WEBHOOK_SECRET. - When sending a webhook, the provider:
- Takes the raw request body as bytes.
- Optionally prefixes it with a timestamp string.
- Calculates an HMAC using a hash function like SHA256.
- Puts the result in a header, for example
X-Signature: sha256=<hex>. - Your backend:
- Reads the raw request body.
- Reads the timestamp, if used.
- Recomputes the HMAC using the same secret.
- Compares your result with the header, using a constant time comparison function.
If the values match, the message is authentic.
In pseudo code:
import hmac
import hashlib
def is_valid_signature(body: bytes, header_signature: str, secret: str) -> bool:
expected = hmac.new(
key=secret.encode("utf-8"),
msg=body,
digestmod=hashlib.sha256
).hexdigest()
return constant_time_compare(expected, header_signature)
Rule: Never trust webhook payloads without verifying a signature or using another strong authentication method.
Rule: Always compare signatures using a constant time compare function to avoid timing attacks.
IP Whitelisting
Some providers let you know which IP ranges they use. You can then allow only these IPs for webhook endpoints.
In practice:
- Web server or firewall checks that the source IP is in the allowed list.
- Your application code still verifies signatures, because IPs can sometimes be spoofed or misconfigured.
IP whitelisting is a useful extra layer, not a replacement for signatures.
No Public Secrets in URLs
Never use secrets directly in URLs like:
https://api.example.com/webhooks/payments?token=my-secret
Reasons:
- URLs often appear in logs and browser history.
- A leaked URL would expose the secret.
If a provider allows only URL based secrets, treat the URL as sensitive and rotate it whenever possible. Prefer header based authentication or signatures.
Handling Webhook Reliability
Webhooks are part of a distributed system. You must handle:
- Retries and duplicates.
- Out of order events.
- Temporary failures.
Idempotency and De Duplication
Providers often retry webhooks when they do not get a 2xx response. Your endpoint might then receive the same event several times.
Your handler must be idempotent. That means applying the same event multiple times leads to the same final state.
You usually achieve this with event ids.
Typical pattern:
- Each event has a unique id, for example
evt_12345. - When you receive an event:
- Check if this id already exists in your database.
- If yes, ignore it or treat it as already processed.
- If no, store it and process it.
Example table to store webhook events:
| Column | Type | Description |
|---|---|---|
| id | text | Event id from provider |
| type | text | Event type, for example payment.succeeded |
| payload | jsonb | Full event body |
| received_at | timestamp | When you received it |
| processed_at | timestamp | When processing completed |
| status | text | pending, succeeded, failed |
Pseudo logic:
def handle_webhook(event):
if already_processed(event["id"]):
return
save_event(event) # mark as pending
try:
apply_business_logic(event)
mark_event_as_succeeded(event["id"])
except Exception:
mark_event_as_failed(event["id"])
raiseIn many systems, saving the event and processing it will be decoupled. You save and enqueue it in the webhook handler, then a background worker performs the heavy work.
Out of Order Events
Events may not arrive in strict time order.
Example:
payment.pendingat 10:00payment.succeededat 10:01- But you receive them in the opposite order because of network delays.
Your code must handle this gracefully, for example:
- When you get a
payment.succeeded, you set the payment status to succeeded regardless of previous state. - When you later receive
payment.pendingfor the same payment, you check current status and ignore the older event.
General rule:
- Think about the state machine of your resource.
- Only allow valid transitions, and ignore or log invalid or obsolete ones.
Quick Acknowledgment vs Heavy Work
Your webhook endpoint should respond quickly. Heavy operations can cause:
- Timeouts at the provider side.
- Retries and duplicates.
- Unnecessary load.
Better approach:
- Perform minimal work in the webhook handler:
- Verify the signature.
- Do basic validation.
- Store the event in the database or enqueue a job.
- Return HTTP 200 or 204.
- Let a background worker perform slow tasks, such as:
- Intensive calculations.
- External API calls.
- Large database updates.
- Sending emails.
This design improves reliability and makes your webhook system more resilient.
Example Webhook Flows
Payment Webhook Flow
Imagine you run an e commerce backend and use a payment provider.
1. During Checkout
- User places an order in your system.
- You create a payment session with the provider.
- You redirect the user to the provider payment page.
- You mark the order as pending in your database.
2. Payment Provider Configuration
In the provider dashboard, you configure:
- Webhook URL:
https://api.shop.com/webhooks/payments - Events:
payment.succeeded,payment.failed - Secret:
WEBHOOK_SECRET=abc123
3. Provider Sends Webhook
After the customer pays:
- Provider creates an event:
payment.succeeded. - It calculates
signature = HMAC_SHA256(secret, raw_body). - It sends
POST /webhooks/paymentswith: - JSON body describing the payment.
- Header
X-Signature: sha256=<signature>.
4. Your Backend Handles It
- Reads the raw body and header.
- Validates the signature.
- Validates JSON fields.
- Checks if the event id is already processed.
- If not, stores the event row and enqueues a "process payment" job.
- Returns
200 OK.
A background job then:
- Marks the payment as succeeded in your database.
- Marks the order as paid.
- Decreases inventory.
- Sends a confirmation email.
If any of these tasks fail, you can retry the job without needing another webhook.
Git Push Webhook Flow
Imagine you have a backend that runs tests whenever code is pushed to your repository.
1. Configure Webhook in Git Service
In your Git hosting platform:
- Webhook URL:
https://ci.example.com/webhooks/git - Events:
push - Secret:
GIT_WEBHOOK_SECRET=xyz789
2. Developer Pushes Code
- Git host generates a
git.pushevent with repo, branch, and commit info. - It signs the payload.
- It sends POST to your
/webhooks/gitendpoint.
3. Your CI Backend
- Validates signature.
- Parses the JSON.
- Checks if branch is
main. - Creates a new CI build record in your database.
- Enqueues a "run tests" job for that commit.
- Returns
204 No Content.
Later, your worker runs a test job and updates the build status.
Working with Third Party Webhook Providers
When you integrate with an external webhook provider, always read their webhook documentation carefully. You should understand:
- Event types: A list of possible
typevalues and what they mean. - Payload format: Field names, nesting, and any optional fields.
- Authentication: How to verify requests:
- Secret and HMAC signature.
- JWT token in a header.
- IP ranges.
- Retries:
- When they retry (for example, 5xx or connection errors).
- How many times and with what delay.
- Timeouts:
- How long you can take to respond.
- Delivery guarantees:
- At least once.
- At most once.
- Exactly once (rare).
You may also find:
- Webhook testing tools in their dashboard.
- Options to replay events to a new endpoint, useful for debugging or migrations.
Local Development and Tunnels
In development, your backend usually runs on localhost, which the provider cannot reach over the internet.
Developers use tunneling tools to expose a local server to the internet, such as:
- ngrok
- localtunnel
- cloudflared tunnel
Typical flow:
- Run local backend on
http://localhost:8000. - Start tunnel: it gives you a public URL, for example
https://abcd.ngrok.io. - Configure webhook URL in provider as
https://abcd.ngrok.io/webhooks/payments. - Now the provider sends requests to your laptop.
These tools often show an interface with:
- Incoming requests.
- Headers and bodies.
- Response codes.
This is very helpful when debugging webhook issues.
Common Pitfalls and Best Practices
Common Pitfalls
- Ignoring security:
- Accepting all POSTs without verifying signatures.
- No IP checks when they are available.
- Heavy work in webhook handler:
- Long running tasks causing timeouts.
- Provider retries because your response is too slow.
- Not handling duplicates:
- Event id not stored.
- Same event applied multiple times, for example customer is charged twice.
- Assuming events are ordered:
- Later events overwriting more recent state.
- Hard failures on unknown event types:
- New event types added by provider break your endpoint.
- No logging or observability:
- Difficult to debug missing or failed events.
Best Practices Checklist
Webhook Best Practices:
- Always verify authenticity
- Use HMAC signatures or other strong mechanisms.
- Use constant time comparison functions.
- Make handlers idempotent
- Use event ids and store processed events.
- Safely handle duplicate deliveries.
- Acknowledge quickly
- Verify and enqueue, then return 2xx.
- Do heavy processing in background jobs.
- Validate payloads
- Check required fields, types, and structure.
- Reject clearly invalid requests with 4xx.
- Handle unknown event types gracefully
- Log and ignore instead of failing the entire request.
- Design for out of order delivery
- Update state using safe transitions.
- Avoid assumptions about chronological order.
- Log important data
- Log event id, type, and processing result.
- Be careful not to log secrets.
- Monitor failures
- Track number of failed events.
- Set alerts for repeated failures.
By following these principles, you can build webhook integrations that are secure, reliable, and easier to maintain, and that work well together with your other REST API endpoints.
Views: 6
KAHIBARO