Gareth Wilson Gareth Wilson

Guide to Segment Webhooks: Features and Best Practices

Published


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, PUT or PATCH), headers, payload mapping and batching switch.
  • Webhooks (Classic) sends every call as a POST to 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

FeatureDetails
ConfigurationSegment app: Connections > Catalog, then Webhooks (Actions) or Webhooks (Classic), connected to one Segment source
DestinationsWebhooks (Actions): up to 5 mappings per destination instance. Classic: up to 5 URLs
HTTP methodsActions: POST, PUT or PATCH per mapping. Classic: POST
BodyJSON. The spec call, or whatever object a custom Actions Data mapping produces
Event fieldBody type: page, screen, identify, track, alias, group or delete
Signature headerX-Signature, sent only when a Shared Secret is set
SchemeHMAC-SHA1 of the request body, hex-encoded, keyed with the Shared Secret
Replay windowNone. No timestamp is sent or signed
BatchingActions only, off by default. Default batch size 1000, maximum 4000. Signs only the first event
Response window5 seconds (stated on the Classic page)
RetriesTimeouts and 5xx responses, for up to four hours with randomized exponential backoff
TLSSelf-signed certificates are rejected
Source IPsNot published

Common events

Every request is one Segment spec call, and the body's type field tells you which:

typeWhat it records
trackAn action a user performed. The action's name is in event and its details in properties
identifyWho a user is, with traits such as email and plan
pageA page view on a website, with properties such as path, url and referrer
screenThe mobile equivalent of a page view, with the screen's name
groupA user's association with a company, account or team, with groupId and traits
aliasA merge of two user identities, from previousId to userId
deleteA 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:

  1. In the Segment app, go to Connections > Catalog, search for Webhooks (Actions) and click Configure Webhooks (Actions).
  2. Select the Segment source whose events you want to forward, and name the destination.
  3. 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.
  4. 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.
  5. 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-Signature HMAC-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-Signature once 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 messageId where 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.