What Is a Webhook?
A webhook is a mechanism that allows one application to notify another application when a specific event occurs. Instead of repeatedly requesting updates, the receiving application exposes an HTTP endpoint, and the sending application delivers an HTTP request containing information about the event.
Webhooks are commonly used for payment notifications, order updates, GitHub events, CI/CD automation, subscription changes, and integrations between SaaS platforms. For example, a payment provider can send a webhook when a payment succeeds, allowing an application to update an order without continuously checking the payment API.
Although a webhook often looks like a simple HTTP POST request, reliable webhook integration requires more than an endpoint. Authentication, signature verification, retries, duplicate delivery, event ordering, and asynchronous processing all become important once webhooks handle production workflows.
Table of Contents
- How Webhooks Work
- Webhooks vs. Other Communication Patterns
- Webhook Payloads and Event Contracts
- Webhook Security
- Reliable Webhook Delivery
- Production Design Example
- Ready-to-Use Example
- Common Mistakes
- Production Checklist
- Frequently Asked Questions
- Conclusion
How Webhooks Work
A webhook connects an event producer to an HTTP endpoint operated by an event consumer. The producer detects an event and sends a request to a previously registered destination URL.
Unlike a traditional API request initiated by the consumer, webhook delivery is initiated by the producer. The receiving application does not need to know exactly when the event will occur.
Webhook Delivery Lifecycle
Consider an e-commerce application integrated with a payment provider. When a customer completes a payment, the payment provider generates an event and sends it to the application's webhook endpoint.
- Registration: The application registers an HTTPS webhook URL with the payment provider.
- Event generation: The provider records a payment event.
- Payload creation: The provider serializes the event into a structured message.
- Authentication: The provider signs the payload using a configured signing mechanism.
- Delivery: The provider sends an HTTP POST request to the registered endpoint.
- Verification: The receiver validates the signature and event structure.
- Acknowledgment: The receiver returns an appropriate HTTP success response after safely accepting the event.
- Processing: The application updates internal state or triggers downstream work.
The webhook sender usually expects a response within a relatively short timeout. A successful response indicates that the event was accepted according to the sender's delivery contract, not necessarily that every downstream business operation has finished.
The exact success codes, timeout values, signature formats, and retry policies depend on the webhook provider.
Webhooks vs. Polling
Polling requires a consumer to repeatedly request the current state from another service. Webhooks reverse that interaction: the producer sends a notification when a relevant event occurs.
Suppose an application checks a payment API every ten seconds to discover whether a payment has completed. Most requests may return unchanged data, consuming network and API capacity without providing new information.
A webhook can notify the application shortly after the payment status changes, avoiding most unnecessary requests.
| Property | Polling | Webhooks |
|---|---|---|
| Initiator | Consumer | Producer |
| Update latency | Depends on polling interval | Depends on event generation and delivery |
| Unchanged-state requests | Common | Usually unnecessary |
| Receiver requirement | Outbound API access | Reachable HTTP endpoint |
| Failure recovery | Query again | Retries, replay, or reconciliation |
| Typical use | State synchronization | Event notifications |
Webhooks are not automatically more reliable than polling. A webhook can be delayed or permanently lost if delivery attempts are exhausted, while polling can recover the latest state during the next successful query.
Critical integrations often use both approaches: webhooks for prompt notifications and periodic reconciliation to detect missed events or inconsistent state.
Webhooks vs. Other Communication Patterns
Webhooks are one form of application-to-application communication. They share characteristics with REST APIs, asynchronous messaging, and publish-subscribe systems, but their operational contracts differ.
The distinction is especially important when choosing between direct HTTP delivery and an internal message broker.
Webhooks vs. REST APIs
A REST API exposes resources and operations that clients invoke when needed. A webhook exposes an HTTP endpoint that another system invokes when an event occurs.
The underlying transport can be identical. Both may use HTTPS, JSON payloads, authentication, and HTTP status codes.
The difference is the direction and purpose of the interaction. An API client asks for information or performs an operation, while a webhook producer announces that something has happened.
Many integrations combine both. A payment webhook reports that a payment changed state, and the receiving application may subsequently call the payment provider's API to retrieve authoritative details.
Webhooks vs. Message Queues and Pub/Sub
Message queues and publish-subscribe systems deliver events through messaging infrastructure. Webhooks deliver notifications directly over HTTP to registered endpoints.
A message broker commonly provides durable storage, consumer coordination, acknowledgments, and configurable delivery semantics. A webhook platform must implement its own delivery tracking and retry policies if those guarantees are required.
Webhooks are particularly useful for communication between independently operated organizations because HTTPS is widely supported and does not require exposing a private message broker.
Inside an application platform, message queues may be more appropriate for reliable asynchronous processing. The differences are covered in What Is Pub/Sub? and What Is a Message Queue?.
Webhook Payloads and Event Contracts
A webhook payload should provide enough information to identify the event, understand its meaning, and process it safely. Sending arbitrary JSON without a documented contract makes integrations difficult to maintain.
A well-designed event envelope separates delivery metadata from the business-specific data associated with the event.
Designing an Event Envelope
A typical webhook event contains a unique event identifier, event type, creation timestamp, schema version, and event data.
{
"id": "evt_7f93b12a",
"type": "payment.succeeded",
"version": "2026-01",
"created_at": "2026-10-09T18:30:00Z",
"data": {
"payment_id": "pay_1842",
"order_id": "ord_9021",
"amount": 4999,
"currency": "USD"
}
}
The event ID is particularly important. It allows the receiver to recognize duplicate deliveries without relying on the HTTP request ID or comparing complete payload bodies.
The event timestamp describes when the event occurred, not necessarily when the webhook was delivered. Network failures and retries may cause delivery minutes or hours later.
Event payloads should avoid unnecessary sensitive data. A notification that a payment succeeded usually does not need to include full payment credentials or private customer information.
Event Versioning and Compatibility
Webhook consumers are often maintained by external teams and may not upgrade immediately when a provider changes its API.
Adding optional fields is usually easier to handle than renaming or removing existing fields. Changing the meaning of an event type can be particularly disruptive because consumers may interpret it as a different business operation.
A provider can publish versioned event schemas, document compatibility rules, and allow consumers to migrate before removing old formats.
Consumers should also handle unknown optional fields gracefully. Strict rejection of every unexpected field can break integrations after harmless schema additions.
Webhook Security
A public webhook endpoint accepts requests from outside the application. Without authentication, an attacker may submit forged payment notifications, account updates, or deployment events.
HTTPS protects the transport, but it does not prove that the request originated from the expected webhook provider. Production receivers need a separate mechanism for verifying message authenticity.
HMAC Signature Verification
A common approach uses HMAC-SHA256 with a shared secret. The sender computes a message authentication code over the payload, and the receiver recomputes it using the same secret.
The receiver must verify the signature against the exact raw request body. Parsing JSON and serializing it again can change whitespace, key order, or formatting and invalidate an otherwise legitimate signature.
import hashlib
import hmac
def verify_signature(
body: bytes,
signature: str,
secret: bytes,
) -> bool:
expected = hmac.new(
secret,
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(
expected,
signature,
)
The use of hmac.compare_digest() avoids ordinary string-comparison timing behavior. It does not replace secret management, replay protection, or verification of the provider's documented signing format.
Some providers sign the timestamp together with the body, while others include signature versions or multiple signatures during secret rotation. The receiver must implement the provider's actual specification rather than assuming every webhook uses the same format.
For broader API trust boundaries and authentication patterns, see API Security Patterns.
Replay Protection and Secret Management
A valid signature proves that a payload was signed using the expected secret, but it does not automatically prevent someone from resending a previously captured valid request.
Replay protection commonly combines a signed timestamp, an acceptable clock-skew window, and persistent event deduplication.
For example, a receiver might reject signed requests whose timestamps differ from the current time by more than five minutes. That threshold is an application policy, not a universal webhook standard.
Signing secrets should be stored in a secret manager or protected environment configuration, never committed to source control. Rotation should support an overlap period when both the old and new secrets are accepted.
SSRF and Endpoint Validation
Webhook providers face an additional security risk: users may register arbitrary destination URLs. If the delivery worker blindly sends requests to those URLs, an attacker may attempt to access internal services or cloud metadata endpoints.
This is a server-side request forgery (SSRF) risk.
Webhook delivery platforms should validate destination URLs, restrict unsafe address ranges, control redirects, and enforce network egress policies.
DNS resolution requires particular care because a hostname may resolve to a safe address during validation and a private address when the connection is established.
Receiver security and sender security are separate concerns. A secure receiver verifies authenticity, while a secure sender also protects its infrastructure from malicious destinations.
Reliable Webhook Delivery
Webhook delivery depends on networks, remote servers, and applications outside the sender's control. Requests can time out, connections can fail, and receivers can process an event successfully without returning a response.
For that reason, webhook systems should be designed around retries, duplicate delivery, and the possibility of delayed or missing events.
Retries and Timeouts
A webhook sender should use a bounded connection timeout and an overall request deadline. If delivery fails, the event can be scheduled for another attempt according to a retry policy.
Exponential backoff with jitter helps avoid repeatedly overwhelming a recovering receiver.
import random
def retry_delay(
attempt: int,
base_seconds: float = 2.0,
max_seconds: float = 300.0,
) -> float:
if attempt < 0:
raise ValueError("Attempt must be non-negative")
maximum = min(
max_seconds,
base_seconds * (2 ** min(attempt, 20)),
)
return random.uniform(0, maximum)
Retries should be bounded by a maximum attempt count or delivery lifetime. Otherwise, a permanently invalid endpoint can accumulate an unlimited backlog.
Not every HTTP response should be treated identically. A timeout or HTTP 503 is commonly retryable, while repeated HTTP 401 or 404 responses may indicate a configuration problem requiring operator attention.
Providers differ in which status codes trigger retries. A receiver should follow the provider's documented acknowledgment contract rather than relying on assumptions.
Idempotency and Duplicate Events
Suppose a receiver processes a payment webhook and commits the order update, but the HTTP response is lost. The sender cannot know whether the event was processed and may retry it.
If the receiver repeats the business operation, the order could be updated twice or a downstream action could run more than once.
Idempotent processing prevents repeated delivery of the same event from producing repeated business effects.
A common implementation stores the event ID in a database table with a unique constraint. The event ID and business update are handled within the same transaction when the database and business model permit it.
CREATE TABLE processed_webhook_events (
event_id TEXT PRIMARY KEY,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
The unique constraint allows the database to arbitrate concurrent attempts to process the same event. A plain application-level check followed by a separate insert is vulnerable to races.
More details about the pattern are available in What Is an Idempotency Key?.
Event Ordering and Recovery
Webhook events may arrive out of order. A subscription cancellation event could reach the receiver before an earlier subscription update if the first delivery was delayed or retried.
Processing events strictly in arrival order can therefore overwrite newer state with older information.
Possible solutions include comparing resource versions, checking event sequence numbers when the provider supplies them, or fetching the latest authoritative resource state from the provider's API.
When event ordering is not guaranteed, the consumer must not assume that the most recently delivered event represents the latest business state.
For critical workflows, periodic reconciliation is valuable. It can detect events that were never delivered successfully and repair inconsistencies between systems.
Production Design Example
Consider a SaaS platform that receives payment webhooks from an external payment provider. The application processes subscription activations, failed payments, refunds, and cancellations.
The platform must acknowledge requests quickly, prevent duplicate business effects, preserve event history, and recover from temporary database or worker failures.
Payment Webhook Architecture
The public webhook endpoint runs behind an HTTPS load balancer. After validating the signature, the receiver stores the event in a durable inbox table and returns a successful acknowledgment.
A background worker processes accepted events and updates subscription state. The worker may also publish internal events for email notifications, analytics, or other services.
The core components are:
- Webhook receiver: Accepts HTTPS requests, verifies signatures, and validates event envelopes.
- Inbox storage: Persists events with unique event identifiers and processing status.
- Background workers: Execute business logic independently of the HTTP request lifetime.
- Retry scheduler: Reattempts transiently failed processing operations.
- Reconciliation job: Compares internal subscription state with the payment provider's authoritative state.
- Monitoring: Tracks delivery failures, queue age, duplicate events, and processing latency.
The receiver should acknowledge the webhook only after the event is durably accepted. Returning HTTP 200 before persistence creates a window in which the event can be lost if the process crashes.
Likewise, performing slow subscription updates before acknowledgment increases the chance of provider timeouts and duplicate deliveries.
Webhook Delivery Pipeline
A reliable receiver separates event acceptance from business processing. This allows short HTTP response times while preserving the ability to retry internal failures.
The inbox pattern is particularly useful because the webhook payload and its processing state can be stored in the same database used by the application.
If processing publishes another event to a message broker, the Transactional Outbox pattern can prevent a database commit from becoming inconsistent with an attempted message publication.
Operationally, the system should monitor the oldest unprocessed event, the number of retrying events, the rate of duplicate deliveries, and the percentage of webhook requests rejected for invalid signatures.
Failed events should remain inspectable. Permanently discarding an event after a few attempts without an alert or recovery path makes financial and subscription workflows difficult to reconcile.
Ready-to-Use Example
The following example implements a signed webhook receiver and a sender using FastAPI, HTTPX, and SQLite. The receiver verifies a timestamped HMAC signature, rejects stale requests, and stores events durably before acknowledging them.
SQLite is sufficient for a local demonstration. Production deployments should use a shared transactional database or another durable inbox mechanism appropriate for the application architecture.
Dependencies and Setup
Install the required packages:
pip install fastapi uvicorn httpx
Set a shared signing secret in the environment. Both the sender and receiver must use the same value.
export WEBHOOK_SECRET="replace-with-a-long-random-secret"
The example signs the timestamp and raw request body together. The signature header uses the format X-Webhook-Signature: sha256=<hex-digest>.
The timestamp header is X-Webhook-Timestamp, containing Unix time in seconds.
FastAPI Webhook Receiver
Save the following code as receiver.py. The endpoint verifies the signature before parsing the JSON payload, then stores the event in an SQLite table with a unique event ID.
import hashlib
import hmac
import json
import os
import sqlite3
import time
from contextlib import contextmanager
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, Field
SECRET = os.environ["WEBHOOK_SECRET"].encode("utf-8")
DATABASE = "webhooks.db"
MAX_AGE_SECONDS = 300
app = FastAPI()
class WebhookEvent(BaseModel):
id: str = Field(min_length=1, max_length=200)
type: str = Field(min_length=1, max_length=200)
created_at: str
data: dict
@contextmanager
def database():
connection = sqlite3.connect(
DATABASE,
timeout=5.0,
)
try:
yield connection
finally:
connection.close()
def initialize_database():
with database() as connection:
connection.execute("""
CREATE TABLE IF NOT EXISTS webhook_events (
event_id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
received_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
connection.commit()
initialize_database()
def verify_webhook(
body: bytes,
timestamp: str,
signature: str,
) -> bool:
try:
sent_at = int(timestamp)
except ValueError:
return False
if abs(time.time() - sent_at) > MAX_AGE_SECONDS:
return False
message = timestamp.encode("utf-8") + b"." + body
expected = hmac.new(
SECRET,
message,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(
f"sha256={expected}",
signature,
)
@app.post("/webhooks/events")
async def receive_webhook(request: Request):
body = await request.body()
timestamp = request.headers.get(
"X-Webhook-Timestamp", ""
)
signature = request.headers.get(
"X-Webhook-Signature", ""
)
if not verify_webhook(body, timestamp, signature):
raise HTTPException(
status_code=401,
detail="Invalid webhook signature",
)
try:
event = WebhookEvent.model_validate_json(body)
except Exception:
raise HTTPException(
status_code=400,
detail="Invalid event payload",
)
with database() as connection:
cursor = connection.execute(
"""
INSERT OR IGNORE INTO webhook_events
(event_id, event_type, payload)
VALUES (?, ?, ?)
""",
(
event.id,
event.type,
json.dumps(event.model_dump()),
),
)
connection.commit()
duplicate = cursor.rowcount == 0
return {
"received": True,
"event_id": event.id,
"duplicate": duplicate,
}
@app.get("/health")
async def health():
return {"status": "ok"}
The unique primary key ensures that repeated deliveries with the same event ID do not create duplicate inbox records. SQLite transactions make the insert durable before the endpoint returns success, subject to the database's configured durability guarantees.
The receiver intentionally does not execute payment business logic inside the HTTP handler. A separate worker should process pending events and update their status.
This local example uses synchronous SQLite access inside an asynchronous endpoint. For production, use an asynchronous database driver or move blocking database operations to a thread pool.
It also lacks request-size limits, secret rotation, structured logging, and a worker implementation. Those are required additions for a complete production integration.
Signed Webhook Sender
Save the following code as sender.py. It creates an event, serializes the payload, calculates a timestamped HMAC signature, and sends the request to the receiver.
import hashlib
import hmac
import json
import os
import time
import uuid
import httpx
SECRET = os.environ["WEBHOOK_SECRET"].encode("utf-8")
URL = "http://127.0.0.1:8000/webhooks/events"
def sign_payload(
body: bytes,
timestamp: str,
) -> str:
message = timestamp.encode("utf-8") + b"." + body
digest = hmac.new(
SECRET,
message,
hashlib.sha256,
).hexdigest()
return f"sha256={digest}"
def send_event(event: dict) -> httpx.Response:
body = json.dumps(
event,
separators=(",", ":"),
).encode("utf-8")
timestamp = str(int(time.time()))
headers = {
"Content-Type": "application/json",
"X-Webhook-Timestamp": timestamp,
"X-Webhook-Signature": sign_payload(
body,
timestamp,
),
}
with httpx.Client(timeout=5.0) as client:
response = client.post(
URL,
content=body,
headers=headers,
)
response.raise_for_status()
return response
event = {
"id": f"evt_{uuid.uuid4().hex}",
"type": "payment.succeeded",
"created_at": "2026-10-09T18:30:00Z",
"data": {
"payment_id": "pay_1842",
"order_id": "ord_9021",
"amount": 4999,
"currency": "USD",
},
}
response = send_event(event)
print(response.json())
# Sending the same event again tests deduplication.
response = send_event(event)
print(response.json())
The second delivery uses the same event ID but a newly generated timestamp and signature. The receiver should accept the authenticated request while reporting that the event is a duplicate.
In a production sender, the event ID must remain stable across retries. Generating a new ID for every delivery attempt defeats receiver-side deduplication.
Running and Testing
Start the receiver:
uvicorn receiver:app --host 127.0.0.1 --port 8000
In another terminal, run the sender:
python sender.py
The first request should return a successful response with "duplicate": false. The second should return "duplicate": true.
To test signature rejection, change the sender's signing secret and repeat the request. The receiver should return HTTP 401.
To inspect persisted events, open the SQLite database:
python - <<'PY'
import sqlite3
connection = sqlite3.connect("webhooks.db")
for row in connection.execute(
"SELECT event_id, event_type, status FROM webhook_events"
):
print(row)
connection.close()
PY
This demonstration establishes the essential receiver boundary: authenticate the request, validate the event, persist it once, and acknowledge acceptance.
For production processing, a worker should claim pending events, execute idempotent business operations, record successful completion, and retry transient failures. The worker must handle crashes between business execution and status updates without repeating irreversible effects.
Common Mistakes
Webhook integrations frequently appear reliable during initial testing because requests arrive in order and the receiver responds immediately. Production failures expose assumptions that were never part of the delivery contract.
Performing Slow Work Before Acknowledgment
A webhook handler that performs several database updates, sends emails, and calls external APIs before responding may exceed the sender's timeout. The sender can retry even when some operations already completed.
Persist the event durably and acknowledge it quickly. Execute slower work through a separate processing pipeline with its own retry and recovery logic.
Trusting Unsigned Payloads
A public endpoint that accepts a payment status solely because the JSON payload says "payment.succeeded" is vulnerable to forged requests.
Verify the provider's documented signature format against the raw body, protect signing secrets, and reject requests that fail authentication.
Assuming Exactly-Once Delivery
Webhook providers commonly retry failed or uncertain deliveries. A receiver may therefore see the same event multiple times, even when the sender behaves correctly.
Use stable event IDs, database uniqueness constraints, and idempotent business operations. Treat duplicate delivery as a normal condition rather than an exceptional one.
Ignoring Event Ordering
Retries and concurrent delivery workers can cause events to arrive out of order. Applying an older update after a newer one may corrupt local state.
Use resource versions, sequence numbers, or authoritative API reconciliation when event ordering matters. Do not infer business chronology from HTTP arrival order.
Production Checklist
Before accepting production webhooks, verify that the receiver can authenticate requests, persist events safely, recover from failures, and tolerate duplicate or delayed delivery.
- Transport: Require HTTPS and restrict accepted request methods and payload sizes.
- Authentication: Verify provider signatures against raw request bodies using constant-time comparison.
- Replay protection: Validate signed timestamps and maintain persistent event deduplication.
- Persistence: Store accepted events durably before returning a successful acknowledgment.
- Processing: Move slow business operations into a retryable background processing pipeline.
- Ordering: Handle delayed and out-of-order events using versions or authoritative state checks.
- Recovery: Provide retries, failed-event inspection, and reconciliation for critical workflows.
- Monitoring: Track signature failures, duplicate events, processing lag, retries, and permanent failures.
Frequently Asked Questions
Webhook behavior depends on the sender's delivery contract and the receiver's processing design. These questions address common expectations about timing, transport, failures, and reliability.
Are Webhooks Real-Time?
Webhooks are event-driven and often delivered shortly after an event occurs, but they do not guarantee immediate delivery. Queueing, retries, network failures, and provider processing delays can introduce latency.
Systems requiring strict latency guarantees should evaluate the provider's documented delivery characteristics rather than assuming webhooks are instantaneous.
Do Webhooks Always Use HTTP POST?
Most webhook implementations use HTTP POST with a JSON payload, but this is a convention rather than an absolute requirement.
Some providers use different content types, signature formats, or request methods. Integrations must follow the provider's documented contract.
What Happens When a Webhook Fails?
Many providers retry unsuccessful deliveries using a configurable or predefined schedule. Retry behavior varies by provider and may depend on response status codes or connection failures.
After retries are exhausted, the event may be marked as failed, retained for manual replay, or discarded according to the provider's policy.
Can Webhooks Be Delivered More Than Once?
Yes. Duplicate delivery is common in at-least-once delivery systems. A sender may retry when the receiver processes an event but fails to return a successful acknowledgment.
Receivers should use unique event identifiers and idempotent processing so repeated delivery does not repeat business effects.
Should Webhooks Be Processed Through a Queue?
For nontrivial production workflows, durable asynchronous processing is usually preferable to executing all business logic inside the HTTP request.
A queue or database-backed inbox allows the receiver to acknowledge events quickly while workers handle retries, concurrency, and downstream failures independently.
Conclusion
Webhooks provide a practical way for applications to react to events without repeatedly polling external APIs. They are especially useful for SaaS integrations, payment notifications, repository automation, and other workflows where one system needs to notify another about state changes.
The HTTP request is only the delivery mechanism. Reliable webhook processing requires authentication, durable event acceptance, duplicate detection, asynchronous processing, retries, and recovery from delayed or missing events.
Key Takeaway: A webhook is an HTTP-based event notification, not a guarantee that an event will be delivered exactly once or in order. Production integrations should treat webhook delivery as an unreliable boundary and build explicit mechanisms for security, idempotency, persistence, and recovery.
More Articles to Read
These related articles explore the communication and reliability patterns that support webhook-based integrations.
Comments (0)