Every outbound webhook includes an x-st-signature header. Verify it before processing the event.
Algorithm
x-st-signature = HMAC_SHA256(signingSecret, rawBody).hex()
| Item | Value |
|---|
| Algorithm | HMAC-SHA256 |
| Key | Your webhook signing secret (e.g. whsecd_...) |
| Message | Exact raw HTTP body bytes |
| Output | Lowercase hex string (64 characters) |
| Header | x-st-signature |
| Algorithm header | x-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
| Pitfall | Result |
|---|
| Pretty-printed JSON | Signature mismatch |
| Re-stringifying parsed JSON | Signature mismatch (key order may differ) |
| Trailing characters on secret | Signature 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).