Guide to RudderStack Webhooks: Features and Best Practices
RudderStack is a customer data platform. It collects events from websites, mobile apps, cloud tools and warehouses, and routes them to the analytics, marketing and storage tools you connect. Its HTTP Webhook destination sends those same events to an endpoint you run, which is how you feed RudderStack data into an internal service that has no ready-made integration.
This guide covers how the HTTP Webhook destination shapes its requests, the message types you receive, how to authenticate requests with HTTP Basic credentials, and the practices that keep a receiver reliable in production.
What are RudderStack webhooks?
In RudderStack, a webhook is a destination. You connect the HTTP Webhook destination to one or more sources, and RudderStack sends each event from those sources to your URL in cloud mode. Web, mobile, server-side, cloud, warehouse and Shopify sources are all supported.
The destination is configurable, so the request your endpoint receives depends on the settings: the method, the body format, whether events are sent as is or mapped, whether they are batched, and which authentication type is used. RudderStack does not sign requests. Of its four authentication types, Basic Auth is the one Hookdeck's RudderStack source checks.
RudderStack also has an older destination called Webhook, which only supports custom headers for authentication. This guide covers the HTTP Webhook destination.
RudderStack webhook features
| Feature | Details |
|---|---|
| Configuration | HTTP Webhook destination, connected to one or more sources |
| Base URL | HTTP or HTTPS. HTTPS needs a valid TLS certificate. Path and query parameters can use JSONPath |
| Method | POST by default. PUT, PATCH, GET and DELETE are also available |
| Body format | JSON, XML or form-encoded |
| Body content | The RudderStack event as is (default), or fields you map with JSONPath |
| Batching | JSON only. An array of up to 100 events, sent when full or after 5 seconds |
| Headers | Custom headers with constant keys, plus user-agent: RudderLabs on every request |
| Authentication | No Auth, Basic Auth, Bearer Token or API Key. No signature |
| Failure visibility | The destination's Live Events tab, with the error response |
Message types
With Send the event payload as is turned on, which is the default, the body is the RudderStack event and its type field says what kind it is.
type | Meaning | Key fields |
|---|---|---|
track | A user action | event (the action's name), properties |
identify | Who the user is | userId, traits |
page | A web page view | name, properties |
screen | A mobile screen view | name, properties |
group | A user joining a company or account | groupId, traits |
alias | Two user identities merged | userId, previousId |
The destination also accepts record and audienceList messages from warehouse and audience sources.
Every event carries common fields such as messageId, anonymousId, userId, timestamp, context and integrations. Here is an illustrative track event, shortened to show the fields a receiver usually reads:
{
"type": "track",
"event": "Order Completed",
"messageId": "f6a1b0e2-2c4d-4b7a-9d3e-0a1b2c3d4e5f",
"anonymousId": "8d872292709c6fbe",
"userId": "user_123",
"timestamp": "2026-10-22T09:15:27.000Z",
"properties": {
"order_id": "ORD-1001",
"revenue": 49.99,
"currency": "GBP"
},
"context": {
"library": { "name": "RudderLabs JavaScript SDK" }
},
"integrations": { "All": true }
}
Three settings change that shape:
- Mappings. If you turn off Send the event payload as is, the body contains only the fields you map, and
typeis missing unless you map it. - Batching. With batching on, the body is a JSON array of events,
[{...}, {...}], rather than one object. - Body format. XML and form bodies are not JSON, and a
GETrequest has no body at all.
Inspect RudderStack webhooks as they arrive. Set a Hookdeck Inspect URL as the destination's Base URL to see the exact method, headers and body your settings produce, with no account or setup required.
Setting up RudderStack webhooks
Pick a username and generate a long random password, for example with openssl rand -hex 32, and store both on your receiver.
- In the RudderStack dashboard, add a source if you don't have one, then add the HTTP Webhook destination and give it a name.
- Set Base URL to your HTTPS endpoint.
- Under Authentication, choose Basic Auth and enter the username and password.
- Leave Method as
POSTand Body Format as JSON unless your endpoint needs something else. - Optionally add path and query parameters, custom headers, mappings and batching.
- Connect the destination to your sources, send a test event, and check the Live Events tab for failures.
RudderStack builds the Authorization: Basic header itself and sends it on every request, so there is no challenge round trip.
To put Hookdeck in front of your service, create a Hookdeck source with the RudderStack type and set its URL as the Base URL. Enter the same Username and Password on the source. Hookdeck still answers with a 200, but a request with missing or wrong credentials is recorded as failed verification and never reaches your destination. The source has POST selected. If you change the destination's method to PUT, PATCH, GET or DELETE, allow that method on the Hookdeck source as well. If you choose Bearer Token or API Key in RudderStack instead of Basic Auth, this source won't verify the request.
Securing RudderStack webhooks
With Basic Auth, each request carries Authorization: Basic base64(username:password). Decode it, split on the first colon, and compare the username and password with your stored values in constant time.
const crypto = require('crypto');
const express = require('express');
function safeEqual(a, b) {
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function verifyBasicAuth(header, username, password) {
if (!username || !password) return false; // never accept when unconfigured
const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header || '');
if (!match) return false;
const decoded = Buffer.from(match[1], 'base64');
const colon = decoded.indexOf(0x3a); // split on the first colon: passwords may contain ':'
if (colon === -1) return false;
const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(username, 'utf8'));
const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(password, 'utf8'));
return userOk && passOk; // evaluate both before deciding
}
const app = express();
app.post('/webhooks/rudderstack', express.raw({ type: '*/*' }), (req, res) => {
const ok = verifyBasicAuth(
req.get('authorization'),
process.env.RUDDERSTACK_WEBHOOK_USERNAME,
process.env.RUDDERSTACK_WEBHOOK_PASSWORD
);
if (!ok) return res.sendStatus(401);
let body;
try {
body = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// With batching on, the body is an array of events.
const events = Array.isArray(body) ? body : [body];
for (const event of events) {
// Skip event.messageId if you have seen it, then route on event.type.
switch (event.type) {
case 'track':
case 'identify':
break;
default:
// Log and acknowledge anything else.
}
}
res.sendStatus(200);
});
The same check in Python:
import base64
import binascii
import hmac
import os
import re
BASIC_PATTERN = re.compile(r"^basic\s+([A-Za-z0-9+/]+={0,2})\s*$", re.IGNORECASE)
def verify_basic_auth(header, username, password) -> bool:
if not username or not password:
return False # never accept when unconfigured
match = BASIC_PATTERN.match(header or "")
if not match:
return False
try:
decoded = base64.b64decode(match.group(1), validate=True)
except (binascii.Error, ValueError):
return False
received_user, sep, received_password = decoded.partition(b":") # first colon only
if not sep:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
user_ok = hmac.compare_digest(received_user, username.encode("utf-8"))
password_ok = hmac.compare_digest(received_password, password.encode("utf-8"))
return user_ok and password_ok
# verify_basic_auth(request.headers.get("authorization"),
# os.environ.get("RUDDERSTACK_WEBHOOK_USERNAME"),
# os.environ.get("RUDDERSTACK_WEBHOOK_PASSWORD"))
Basic credentials prove the sender knows the password. They do not cover the body and carry no timestamp, so a modified or replayed request still passes. RudderStack accepts plain HTTP Base URLs, and over HTTP the credentials and your users' data travel in clear text, so always use HTTPS. Use a long random password and rotate it in both places if it might have leaked.
Make RudderStack webhooks production-ready. Hookdeck Event Gateway checks the Basic credentials, absorbs traffic spikes, and delivers each event to your service at a rate it can handle.
RudderStack webhook limitations and pain points
The request shape depends on settings
The Problem: A handler that worked yesterday fails after someone turns on batching, edits a mapping or changes the body format.
Why It Happens: The method, body format, mappings and batching are all destination settings that anyone with access can change, and each one changes what your endpoint receives.
Workarounds:
- Accept both a single object and an array, as the example above does.
- Return
400for a body you can't parse, so the failure shows in Live Events instead of being silently dropped. - Agree who owns the destination settings and review changes to them.
How Hookdeck Can Help: Hookdeck stores every request with its method, headers and body, so you can see exactly when the shape changed. Transformations can normalise single events and batches into one shape before they reach your service.
Event volume follows your traffic
The Problem: A campaign or a busy day sends a surge of track and page events, and your endpoint slows down or falls over.
Why It Happens: RudderStack forwards events as fast as sources produce them. Batching cuts the request count but not the number of events.
Workarounds:
- Turn on batching to send up to 100 events per request.
- Acknowledge quickly and process from a queue.
- Drop message types you don't need before they reach your endpoint.
How Hookdeck Can Help: Hookdeck accepts the surge, queues it, and delivers to your service at a max delivery rate you set. Filters can drop message types you don't use before they reach your service.
Retry behaviour is not on the destination page
The Problem: You don't know how long RudderStack keeps retrying when your endpoint is down, or when it gives up.
Why It Happens: The HTTP Webhook setup guide documents the Live Events tab for failures, but not the retry schedule or request timeout.
Workarounds:
- Watch Live Events and alert on errors from your endpoint.
- Keep your endpoint's response time low so a slow response isn't treated as a failure.
How Hookdeck Can Help: Hookdeck accepts and stores each request as it arrives, then retries delivery to your service on a schedule you choose with retries. You can replay events after an outage, and issues alert you when deliveries fail.
Mapped bodies lose the event type
The Problem: With custom mappings, the handler can no longer tell a track from an identify.
Why It Happens: When Send the event payload as is is off, only mapped fields are sent.
Workarounds:
- Map
$.type,$.eventand$.messageIdinto the body, or into the path or query string.
Best practices
Deduplicate on messageId
Every RudderStack event has a messageId. Use it as the idempotency key so a retried request or a replayed batch doesn't count an event twice. See our guide to webhook idempotency.
Handle each event in a batch on its own
When batching is on, one bad event shouldn't fail the whole request. Validate and queue events one at a time, and record the ones you skip.
Order by timestamp, not arrival
Events can arrive late, especially from mobile devices that were offline. Use the event's timestamp when ordering or updating state.
Acknowledge fast
Return 200 once the request is authenticated and process it from a queue. See why to process webhooks asynchronously.
Conclusion
RudderStack's HTTP Webhook destination forwards customer events to your endpoint, and its settings decide the method, body format, mappings and batching. Use Basic Auth over HTTPS, compare the credentials in constant time, accept both single events and batches, and deduplicate on messageId.
Hookdeck Event Gateway checks the Basic credentials, smooths out traffic spikes, and keeps every event until your service has processed it.
Get started with Hookdeck for free and handle RudderStack webhooks reliably in minutes.