Skip to main content
Every outbound webhook includes an x-st-signature header. Verify it before processing the event.

Algorithm

x-st-signature = HMAC_SHA256(signingSecret, rawBody).hex()
ItemValue
AlgorithmHMAC-SHA256
KeyYour webhook signing secret (e.g. whsecd_...)
MessageExact raw HTTP body bytes
OutputLowercase hex string (64 characters)
Headerx-st-signature
Algorithm headerx-st-signature-alg: HMAC_SHA256_HEX
Sign the exact raw body bytes received over the wire. Do not re-format, pretty-print, or re-serialize parsed JSON — any change produces a different signature.

Payload envelope

{
  "id": "<outboxId>",
  "type": "<event_type>",
  "data": { ... }
}
The body is compact JSON from JSON.stringify() with a fixed key order in data (see Events).

Node.js example

import crypto from "crypto";

function verifyWebhookSignature({ rawBody, signingSecret, signature }) {
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);

  if (received.length !== expectedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(received, expectedBuffer);
}

// Express example — use raw body middleware
app.post("/webhooks/st", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-st-signature"];
  const isValid = verifyWebhookSignature({
    rawBody: req.body,
    signingSecret: process.env.WEBHOOK_SIGNING_SECRET,
    signature,
  });

  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());
  // Process event...
  res.sendStatus(200);
});

Python example

import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signing_secret: str, signature: str) -> bool:
    expected = hmac.new(
        signing_secret.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Common pitfalls

PitfallResult
Pretty-printed JSONSignature mismatch
Re-stringifying parsed JSONSignature mismatch (key order may differ)
Trailing characters on secretSignature mismatch

Endpoint verification

When you create or verify a webhook in the dashboard, a webhook.endpoint_verification event may be sent to confirm your endpoint accepts POST requests and returns HTTP 2xx.

Response requirements

Your endpoint must:
  • Accept POST with Content-Type: application/json
  • Return HTTP 2xx within 5 seconds
  • Verify the signature before processing
Non-2xx responses and timeouts count as delivery failures and trigger retries with incremental backoff (60s first retry, then +40s each time).