A webhook endpoint is a public HTTPS URL on your server that receives event-driven HTTP POST requests from another system. Instead of your application repeatedly polling a third-party API to ask "is there anything new?", the third-party system pushes notifications to your endpoint the moment an event occurs. The endpoint is the inbound side of the contract; the webhook is the message sent to it.
In a typical 2026 SaaS stack, a single application has 5-30 webhook endpoints — one per integration partner sending events (Stripe payments, GitHub commits, Slack messages, Twilio SMS, AI inference completions). Getting webhook endpoints right is one of the highest-ROI engineering investments in any integration-heavy product, and getting them wrong is one of the most common sources of production incidents. This guide covers the definition, the difference between webhooks and APIs, how to build a webhook endpoint correctly in Node.js and Python, the security model, and how to debug when things go wrong.
Definition: Webhook Endpoint vs Webhook vs API
Three terms get confused. Clean definitions:
- API endpoint — a URL on a server you control that other systems call when they need information from you (request-response, you respond on demand).
- Webhook — an HTTP POST request a sender system makes to a receiver system to notify it of an event.
- Webhook endpoint — the URL on the receiver side that accepts those incoming POSTs.
A webhook is a message; a webhook endpoint is the address that receives it. The two are sometimes used interchangeably ("our webhook URL") but the precise meaning is "the URL where webhooks are delivered."
Webhooks vs APIs: The Key Difference
| Dimension | Traditional API | Webhook |
|---|---|---|
| Direction | Client pulls from server | Server pushes to client |
| Trigger | Caller decides when | Event in source system decides when |
| Latency for new data | Polling interval | Near-real-time |
| Server load | High (polling overhead) | Low (only when event occurs) |
| Endpoint location | On the API provider | On the consumer |
| Auth direction | Caller authenticates to API | Sender authenticates to webhook |
| Use cases | Read on demand, CRUD operations | Notifications, integrations, automation |
The shorthand: APIs are pull, webhooks are push. A polling-only architecture talking to Stripe to check for new payments would hit Stripe's API every minute; a webhook-based architecture sits idle until a payment occurs and then receives a single POST.
A Webhook Endpoint, Concretely
When Stripe processes a payment in your account, Stripe sends an HTTP POST to a URL you configured in Stripe's dashboard:
POST https://api.yourapp.com/webhooks/stripe HTTP/1.1
Host: api.yourapp.com
Content-Type: application/json
Stripe-Signature: t=1730912934,v1=5257a869e7ecebed...
{
"id": "evt_1NjkN02eZvKYlo2C0ZqRiHGw",
"type": "payment_intent.succeeded",
"data": { "object": { "id": "pi_3NjkN02eZvKYlo2C0...", "amount": 2000, "currency": "usd" } }
}
https://api.yourapp.com/webhooks/stripe is the webhook endpoint. Your server receives this POST, validates the signature, processes the event, and returns a 2xx response within Stripe's timeout window (15 seconds for Stripe).
Three properties make a URL a valid webhook endpoint:
- Publicly reachable over HTTPS. The sender must be able to make a TCP connection.
- Accepts HTTP POST with whatever content type the sender uses (usually
application/json, sometimesapplication/x-www-form-urlencoded). - Returns 2xx within the sender's timeout to acknowledge receipt.
How to Create a Webhook Endpoint in Node.js
A minimal but production-realistic webhook endpoint in Express:
import express from 'express';
import crypto from 'crypto';
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// IMPORTANT: use raw body for signature validation
app.post(
'/webhooks/example',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.header('X-Webhook-Signature');
const timestamp = req.header('X-Webhook-Timestamp');
if (!signature || !timestamp) {
return res.status(400).send('Missing signature headers');
}
// Reject requests older than 5 minutes (replay protection)
const ageSeconds = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (ageSeconds > 300) {
return res.status(401).send('Request too old');
}
// Compute expected signature
const signedPayload = `${timestamp}.${req.body.toString('utf8')}`;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
const valid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
if (!valid) {
return res.status(401).send('Invalid signature');
}
// Acknowledge fast, process async
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).send('ok');
// Hand off to async worker — do not block the response
queueForProcessing(event);
},
);
app.listen(3000);
Three patterns to internalize from this snippet:
- Raw body for signature validation. Body-parser middleware will mutate the body and break signature checks. Use
express.rawand parse JSON yourself after validating. - Replay protection via timestamp. Reject anything older than 5 minutes. Without this, a captured webhook can be replayed indefinitely.
- Acknowledge fast, process async. Return 2xx within ~1 second; do the actual work in a queue. Webhook senders retry on slow responses, which causes duplicate processing.
How to Create a Webhook Endpoint in Python
The same pattern in FastAPI:
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException, Header
import os
import json
app = FastAPI()
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"].encode()
@app.post("/webhooks/example")
async def handle_webhook(
request: Request,
x_webhook_signature: str = Header(None),
x_webhook_timestamp: str = Header(None),
):
if not x_webhook_signature or not x_webhook_timestamp:
raise HTTPException(400, "Missing signature headers")
# Replay protection
age = int(time.time()) - int(x_webhook_timestamp)
if age > 300:
raise HTTPException(401, "Request too old")
# Raw body required for signature
raw_body = await request.body()
signed_payload = f"{x_webhook_timestamp}.{raw_body.decode()}".encode()
expected = hmac.new(WEBHOOK_SECRET, signed_payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, x_webhook_signature):
raise HTTPException(401, "Invalid signature")
event = json.loads(raw_body)
# Hand off to async worker — Celery, RQ, Arq, etc.
enqueue_processing.delay(event)
return {"status": "ok"}
The structure mirrors the Node.js version exactly — get raw body, check timestamp, check HMAC with constant-time comparison, acknowledge fast, process async.
Webhook Security: The Five Things You Must Get Right
Webhook endpoints are public URLs receiving untrusted input. Security is non-negotiable.
1. HMAC Signature Validation
Every webhook from a serious provider includes a signature header computed from the request body and a shared secret. Validate it on every request. Use constant-time comparison (crypto.timingSafeEqual in Node, hmac.compare_digest in Python) to prevent timing attacks.
2. Replay Protection
Include a timestamp in the signed payload and reject requests older than a small window (5-10 minutes typical). Otherwise an attacker who captures a single valid webhook can replay it indefinitely.
3. HTTPS Only
Never accept webhooks over plain HTTP. The signature does not protect the payload from being read in transit; the body is in plaintext. TLS protects it.
4. Idempotency
Webhook senders retry on failure or timeout. The same event ID may arrive 2-5 times in normal operation. Make your processing idempotent — store processed event IDs (in Redis with a TTL, or in a database table with a unique index) and skip duplicates.
5. IP Allowlisting (Where Appropriate)
For highly sensitive endpoints, accept POSTs only from the sender's documented IP ranges. Stripe, GitHub, Twilio, and most major providers publish their outbound IP CIDRs. This is belt-and-suspenders alongside HMAC; it does not replace it.
Common Webhook Failures and How to Debug Them
Five failure modes that account for 90% of webhook bugs we have debugged:
-
Body-parser middleware changing the body before signature check. Symptom: signatures always fail. Fix: use raw body for signature check, parse JSON only after.
-
The endpoint times out under load. Symptom: senders retry, you process duplicates. Fix: acknowledge with 2xx in under 1 second, process work in a queue.
-
TLS misconfiguration. Symptom: senders fail to deliver, no entry in your logs. Fix: check the sender's dashboard for delivery error messages; common causes are expired certs, missing intermediate certs, or weak ciphers.
-
Implicit content-type assumptions. Symptom: parsing crashes on some webhooks. Fix: check
Content-Typeheader explicitly and handle JSON, form-encoded, and (for some providers) XML. -
Idempotency missing or broken. Symptom: customer charged twice on a Stripe event, or notification sent multiple times. Fix: deduplicate on event ID before processing, with a persistence store outside the request lifecycle.
Debugging Tools
The fastest way to debug a webhook endpoint in 2026:
- Webhook.site or RequestBin — public webhook receivers that capture and display incoming POSTs. Useful for verifying what the sender is actually sending.
- ngrok or Cloudflare Tunnel — expose your local development server publicly so the sender can deliver to your machine.
- Provider dashboards — Stripe, GitHub, Twilio, and most major providers show delivery logs with response codes, latency, and retry attempts. Always check there first.
- Synthetic test events — every serious webhook provider lets you send a test event from the dashboard. Use it before depending on real events.
Webhook Endpoint Best Practices
A condensed checklist for any webhook endpoint going to production:
| Practice | Why |
|---|---|
| HTTPS with valid TLS | In-transit confidentiality |
| HMAC signature validation | Sender authenticity |
| Constant-time signature comparison | Resist timing attacks |
| Timestamp replay protection (5-10 min) | Block replayed captures |
| Raw body for signature | Avoid parser-induced mismatches |
| Idempotency on event ID | Tolerate sender retries |
| 2xx response in under 1 second | Avoid sender retries from slowness |
| Async processing in a queue | Decouple ack from work |
| IP allowlisting (where supported) | Defense in depth |
| Structured logging of every webhook | Debuggability |
| Monitoring alerts on 4xx/5xx rate | Catch outages fast |
| Test events in CI | Catch regressions before deployment |
Webhook Endpoints in AI Workflows
Webhooks have become central to AI workflows in 2026 because long-running inference operations (video generation, fine-tuning, batch inference) cannot fit inside an HTTP request-response cycle.
The 2026 pattern most AI providers expose:
- Client POSTs a generation request to the AI provider with a
webhook_urlin the body. - Provider returns immediately with a job ID.
- Job runs for seconds to hours.
- On completion, provider POSTs the result to the client's webhook endpoint.
Sora, Veo, Runway, fine-tuning APIs on every major provider, and AWS Bedrock batch inference all use this pattern. If you are building any AI-heavy product, you are running webhook endpoints whether you call them that or not.
For multi-provider AI orchestration that abstracts webhook handling across providers see Swfte Connect — every supported model's webhook delivery is normalized into a single event stream. For more on the underlying model landscape see our April 2026 AI model releases roundup and the Swfte AI leaderboard.
FAQ
What is the difference between an API and a webhook endpoint?
An API is something you call to request information; a webhook endpoint is something you host that another system calls to notify you of events. APIs are pull-based and on-demand; webhooks are push-based and event-driven. Most modern systems use both — APIs for synchronous queries and webhooks for asynchronous notifications.
Is a webhook endpoint the same as a REST endpoint?
A webhook endpoint is a specific kind of REST endpoint — it accepts HTTP POST and returns 2xx on success — but it is consumed by a sender notifying you, not by a client requesting data from you. The implementation looks similar; the role is different. A REST API endpoint is "called by clients you serve"; a webhook endpoint is "called by senders notifying you."
What HTTP method does a webhook use?
Almost always POST. A few legacy systems use PUT or GET (with query-string payloads). When designing a new webhook integration, use POST with a JSON body. POST is correct because webhooks deliver new event data, which semantically maps to "create."
How do I test a webhook endpoint locally?
Use a tunnel service: ngrok (ngrok http 3000), Cloudflare Tunnel, or VS Code Dev Tunnels. The tunnel exposes your local server through a public HTTPS URL. Configure that URL in the sender's dashboard, trigger a test event, and observe locally. Webhook.site is a complementary tool when you only need to inspect what the sender is sending.
How do I secure a webhook endpoint?
Five layers in order of priority: (1) HTTPS only, (2) HMAC signature validation with constant-time comparison, (3) timestamp-based replay protection rejecting requests older than 5-10 minutes, (4) idempotency on the event ID, (5) IP allowlisting where the sender publishes IP ranges. Skip any of these and you have a vulnerability.
What status code should a webhook endpoint return?
Return any 2xx (200, 201, 204) on successful receipt. Return 4xx for permanent failures the sender should not retry (signature invalid, malformed payload, unrecognized event). Return 5xx for transient failures the sender should retry. Most providers retry on any non-2xx with exponential backoff.
How long should a webhook endpoint take to respond?
Under 1 second ideally; under 15 seconds always. Most providers (Stripe, GitHub, Twilio) timeout at 10-15 seconds and treat anything slower as a failure. Acknowledge with 2xx fast, then process the event asynchronously in a queue.
Building AI workflows that depend on webhook callbacks? Swfte Connect normalizes webhook handling across every major AI provider. For ongoing model rankings see the Swfte AI leaderboard; for pricing changes see the AI pricing trends page.