Guide to Segment Webhooks: Features and Best Practices
Segment webhooks let you send the customer data that Segment collects (page views, identified users, tracked actions) to your own HTTP endpoint as it arrives. If you run Twilio Segment as your customer data platform, a Webhooks destination is the simplest way to feed that event stream into a service of your own, such as a CRM sync, a fraud check or an internal analytics pipeline.
This guide covers the two Webhooks destinations, the spec calls they forward, how the optional X-Signature header is built, why batching and signing do not mix, and the best practices for running a Segment receiver in production.
What are Segment webhooks?
Segment does not have a list of webhook events of its own. Instead, a Webhooks destination forwards the spec calls that a connected Segment source collects: each track, identify, page and so on becomes an HTTP request to your URL, with the call as a JSON body.
There are two destinations that do this:
- Webhooks (Actions) is the current one. You build up to five mappings, and each mapping has its own trigger, URL, HTTP method (
POST,PUTorPATCH), headers, payload mapping and batching switch. - Webhooks (Classic) sends every call as a
POSTto up to five URLs, with optional static headers.
Both destinations sign requests in the same way, and only if you give them a Shared Secret. Segment then sends an X-Signature header containing a hex HMAC-SHA1 of the request body. If you leave the secret empty, requests arrive unsigned.
Segment webhook features
| Feature | Details |
|---|---|
| Configuration | Segment app: Connections > Catalog, then Webhooks (Actions) or Webhooks (Classic), connected to one Segment source |
| Destinations | Webhooks (Actions): up to 5 mappings per destination instance. Classic: up to 5 URLs |
| HTTP methods | Actions: POST, PUT or PATCH per mapping. Classic: POST |
| Body | JSON. The spec call, or whatever object a custom Actions Data mapping produces |
| Event field | Body type: page, screen, identify, track, alias, group or delete |
| Signature header | X-Signature, sent only when a Shared Secret is set |
| Scheme | HMAC-SHA1 of the request body, hex-encoded, keyed with the Shared Secret |
| Replay window | None. No timestamp is sent or signed |
| Batching | Actions only, off by default. Default batch size 1000, maximum 4000. Signs only the first event |
| Response window | 5 seconds (stated on the Classic page) |
| Retries | Timeouts and 5xx responses, for up to four hours with randomized exponential backoff |
| TLS | Self-signed certificates are rejected |
| Source IPs | Not published |
Common events
Every request is one Segment spec call, and the body's type field tells you which:
type | What it records |
|---|---|
track | An action a user performed. The action's name is in event and its details in properties |
identify | Who a user is, with traits such as email and plan |
page | A page view on a website, with properties such as path, url and referrer |
screen | The mobile equivalent of a page view, with the screen's name |
group | A user's association with a company, account or team, with groupId and traits |
alias | A merge of two user identities, from previousId to userId |
delete | A user deletion request. Documented on the Classic destination only |
Segment's Actions page lists Page, Alias, Group, Identify and Track as the calls the destination accepts. The Classic page also prints Screen and Delete examples.
The value of event on a track call comes from your own tracking plan, so there is no fixed list of track events to subscribe to. This is Segment's Track example as the Classic page prints it:
{
"version": 1,
"type": "track",
"userId": "019mr8mf4r",
"event": "Purchased an Item",
"properties": {
"revenue": "39.95",
"shippingMethod": "2-day"
},
"timestamp": "2012-12-02T00:30:08.276Z"
}
The published examples are trimmed. Real spec calls carry more of Segment's common fields, such as messageId, anonymousId and context. On the Actions destination, the Data field of a mapping decides the body. Its default is the whole event, but a custom mapping can replace it with any object, including one that has no type at all.
If you use Engage, computed traits and audiences reach a Webhooks destination as identify calls, one for each user added to or removed from an audience, with a true or false trait named after the audience.
Setting up Segment webhooks
For the Webhooks (Actions) destination:
- In the Segment app, go to Connections > Catalog, search for Webhooks (Actions) and click Configure Webhooks (Actions).
- Select the Segment source whose events you want to forward, and name the destination.
- On Settings > Advanced Settings, set a Shared Secret if you want signed requests. Store the same value server-side, for example as
SEGMENT_SHARED_SECRET. - Add a mapping. Choose the trigger (which calls it fires on), then fill in the Send action: your URL, the method, any headers, and the Data mapping. Leave Enable Batching? off if you are verifying signatures.
- Enable the mapping and the destination.
For the Classic destination, add Webhooks from the catalog, connect it to a source, enter up to five URLs with any static headers, and set the sharedSecret setting under the advanced options.
To check the pipeline end to end, Segment suggests connecting a Webhooks destination to a source, sending events, and comparing what the source debugger shows with what your endpoint receives.
Securing Segment webhooks
When a Shared Secret is set, compute an HMAC-SHA1 of the raw request body with the secret as the key, encode it as hex, and compare it with X-Signature in constant time. Segment's own Classic sample re-serialises the parsed body with JSON.stringify(req.body), which only works if your serialiser produces exactly the same bytes as Segment's. Hashing the raw body avoids that problem.
Batching is the exception. With Enable Batching? on, the body is a JSON array, and Segment signs only the first event in it rather than the whole request. The fallback below handles that case, but it authenticates only the first event in the batch. The safer choice is to keep batching off.
const crypto = require('crypto');
const express = require('express');
function hmacMatches(content, signature, secret) {
const expected = Buffer.from(
crypto.createHmac('sha1', secret).update(content).digest('hex'),
'utf8'
);
const received = Buffer.from(signature.trim().toLowerCase(), 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
function verifySegmentSignature(rawBody, signature, secret) {
if (!signature || !secret) return false;
// Single event: the signature covers the raw body.
if (hmacMatches(rawBody, signature, secret)) return true;
// Batched delivery: Segment signs only the first event in the array.
// Only that first event is authenticated.
try {
const parsed = JSON.parse(rawBody.toString('utf8'));
return (
Array.isArray(parsed) &&
parsed.length > 0 &&
hmacMatches(JSON.stringify(parsed[0]), signature, secret)
);
} catch {
return false;
}
}
const app = express();
app.post('/webhooks/segment', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.SEGMENT_SHARED_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifySegmentSignature(req.body, req.get('x-signature'), secret)) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body.toString('utf8'));
const events = Array.isArray(payload) ? payload : [payload];
// Route each event on event.type. Persist first, then process asynchronously.
res.sendStatus(200);
});
The same check in Python, for the unbatched case:
import hashlib
import hmac
import os
def verify_segment_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha1).hexdigest()
received = signature_header.strip().lower().encode("utf-8")
return hmac.compare_digest(received, expected.encode("utf-8"))
# In FastAPI: raw_body = await request.body()
# verify_segment_signature(raw_body, request.headers.get("x-signature"),
# os.environ["SEGMENT_SHARED_SECRET"])
If you need the batched fallback in Python, take the exact bytes of the first array element from the raw body rather than calling json.dumps, which formats numbers and Unicode differently from JavaScript's JSON.stringify.
To test your verifier, use the secret s3cr3t and this exact body:
{"type":"track","event":"Purchased an Item","userId":"019mr8mf4r"}
The signature should be:
5b7090f7dc83c3830f936a5fbef6e9e2d2cc66dc
Make Segment webhooks production-ready. Hookdeck Event Gateway verifies the
X-SignatureHMAC-SHA1 header on every Segment request, queues each event durably, and gives you retries and replay on top of Segment's four-hour retry window.
Hookdeck's Segment source type verifies X-Signature against the raw body, so batched requests fail verification there too, and the source's setup notes ask you to turn batching off. Verification is optional on the source: if you didn't set a Shared Secret in Segment, leave verification off. The Segment source accepts POST only, so set each Actions mapping's method to POST when it points at Hookdeck.
Segment webhook limitations and pain points
Batching breaks signature verification
The Problem: Verification works in testing, then every request fails once batching is turned on.
Why It Happens: With batching enabled, the body is an array of events but X-Signature is the HMAC of the first event only. Any verifier that hashes the request body, which is what the scheme otherwise requires, cannot match.
Workarounds:
- Leave Enable Batching? off on every mapping that sends to a verifying endpoint.
- If you need batching, verify the first element as in the sample above, and treat the rest of the batch as unauthenticated.
How Hookdeck Can Help: Hookdeck's queue absorbs bursts on your side, so you can leave Segment's batching off and still deliver at a rate your service can handle, with Hookdeck's rate limiting controlling how fast events reach it.
Signing is optional and easy to skip
The Problem: An endpoint that accepts unsigned requests will accept anything anyone sends it.
Why It Happens: The Shared Secret field is optional on both destinations. Without it, Segment sends no X-Signature header at all, and nothing in the request proves where it came from.
Workarounds:
- Set a Shared Secret before you go live, and reject requests without
X-Signatureonce you have. - Add a static secret header in the mapping's headers as a second check if you want one.
How Hookdeck Can Help: With verification turned on, Hookdeck rejects requests that lack a valid X-Signature before they reach your service.
No timestamp, no replay protection
The Problem: A captured request still verifies if someone sends it again later.
Why It Happens: The signature covers the body and nothing else. Segment sends no signed timestamp, so there is no freshness check to make.
Workarounds:
- Make handlers idempotent, keyed on
messageIdwhere your deliveries include it. - Keep the endpoint on HTTPS so requests cannot be captured in transit.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated requests based on the fields you choose, such as messageId.
A five-second budget
The Problem: Slow handlers time out, and Segment retries requests your service already processed.
Why It Happens: Segment expects a response within 5 seconds and treats a timeout like a 5xx, retrying for up to four hours.
Workarounds:
- Verify, store the event, return
200, and do the work afterwards. - Deduplicate so retried calls do not run twice.
How Hookdeck Can Help: Hookdeck acknowledges Segment as soon as the request arrives and then delivers to your service with its own timeouts and retry schedule, so a slow handler no longer triggers Segment's retries.
Classic stops retrying once one URL succeeds
The Problem: With several Classic URLs, one endpoint can miss events permanently.
Why It Happens: Segment considers a message processed if any of the configured URLs accepts it, and does not retry the others.
Workarounds:
- Use one URL per Classic destination, or move to the Actions destination with one mapping per endpoint.
- Fan out from a single receiver that you control.
How Hookdeck Can Help: Point Segment at one Hookdeck source and add a connection for each destination. Hookdeck delivers to and retries each destination independently.
Best practices
Verify against the raw body
The signature covers the bytes Segment sent. In Express, use express.raw() on the webhook route and parse after verification. In FastAPI, read await request.body(). Re-serialising the parsed JSON, as Segment's Classic snippet does, only matches when your serialiser behaves exactly like Segment's.
Route on type, then on event
Switch on type first. For track calls, route on event, which is your own tracking-plan name. Log and acknowledge any type you do not handle, and do not assume type is present if anyone on your team edits the Actions Data mapping.
Keep the Data mapping at its default
The default mapping sends the whole event. A custom mapping is useful for trimming payloads, but it can remove the fields your receiver depends on, and nothing in the request tells you which mapping produced it.
Acknowledge fast and process asynchronously
Return a 2xx within Segment's 5-second window, then do the real work from a queue. See why to process webhooks asynchronously.
Deduplicate retried calls
Segment retries timeouts and 5xx responses for up to four hours, so the same call can arrive more than once. Segment's spec defines messageId as a unique identifier for each message. Store it before processing and skip any you have seen. See our guide to webhook idempotency.
Use a publicly trusted certificate
Segment rejects self-signed certificates and sends nothing to an endpoint that presents one. Use a certificate from a public CA.
Conclusion
Segment forwards spec calls rather than events of its own, so your receiver routes on type and, for track calls, on your own event names. Set a Shared Secret so every request carries X-Signature, verify the hex HMAC-SHA1 against the raw body, keep batching off unless you accept that only the first event is authenticated, and answer within five seconds.
Hookdeck Event Gateway sits between Segment and your service, verifies X-Signature, and queues every call so a slow or offline receiver does not cost you events.
Get started with Hookdeck for free and handle Segment webhooks reliably in minutes.