Webhook Signature Verification
Each webhook delivery is signed with HMAC-SHA256 so you can verify that the request came from Truss and has not been replayed or tampered with.
Obtaining Your Signing Secret
When webhook signing is enabled for your subscriber, Truss assigns a signing secret: a 64-character hexadecimal string (32 random bytes) unique to your webhook endpoint.
Contact Truss to retrieve your signing secret. There is no self-service endpoint for this today. If you need to rotate your secret, contact Truss and we will provision a new one.
Important: Treat your signing secret like a password. Store it in a secrets manager, never commit it to source control, and never log it.
The Signature Header
When a signing secret is configured, every webhook delivery includes an X-Webhook-Signature header:
X-Webhook-Signature: t=1700000000,v1=a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
| Component | Description |
|---|---|
t |
Unix timestamp (seconds) at the time Truss sent the webhook |
v1 |
HMAC-SHA256 signature as a lowercase hex string (64 characters) |
The v1 prefix is a version identifier. Future signature schemes may use additional version keys in the same header.
If no signing secret is configured for your subscriber, the X-Webhook-Signature header is omitted entirely.
What Is Signed
The signature is computed over the UTF-8 bytes of:
{timestamp}.{raw_request_body}
Where {timestamp} is the value of t from the header and {raw_request_body} is the exact JSON string sent in the HTTP request body — not a re-serialized version of a parsed object.
Important: You must verify the signature against the raw request body bytes as received over the wire. Parsing JSON and re-serializing it (even with the same data) can change key order or spacing and will cause verification to fail.
Verifying a Signature
- Read the raw request body before parsing JSON.
- Read the
X-Webhook-Signatureheader. Reject the request if it is missing. - Parse the header: split on
,, then split each part on the first=to extracttandv1. - Reject the request if
|now - t|exceeds your tolerance window (Truss recommends 300 seconds). - Compute
HMAC_SHA256(signing_secret, "{t}." + raw_body)and compare the resulting hex digest tov1using a constant-time comparison. - Only after verification succeeds, parse the JSON body and process the event.
Examples
Use this helper to verify the X-Webhook-Signature header:
const crypto = require('crypto');
const TOLERANCE_SECONDS = 300;
// Assumes rawBody is the exact, unparsed request body as received from Truss.
function verifyTrussSignature(rawBody, signatureHeader, signingSecret) {
if (!signatureHeader) {
return false;
}
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('=', 2))
);
const timestamp = parseInt(parts.t, 10);
const expectedSignature = parts.v1;
if (!timestamp || !expectedSignature) {
return false;
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) {
return false;
}
const signedPayload = `${timestamp}.${rawBody}`;
const computedSignature = crypto
.createHmac('sha256', signingSecret)
.update(signedPayload, 'utf-8')
.digest('hex');
const expectedBuffer = Buffer.from(expectedSignature, 'utf-8');
const computedBuffer = Buffer.from(computedSignature, 'utf-8');
return (
expectedBuffer.length === computedBuffer.length &&
crypto.timingSafeEqual(expectedBuffer, computedBuffer)
);
}
import hmac
import hashlib
import time
TOLERANCE_SECONDS = 300
# Assumes raw_body is the exact, unparsed request body bytes as received from Truss.
def verify_truss_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
if not signature_header:
return False
parts = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp = int(parts.get("t", 0))
expected_signature = parts.get("v1")
if not timestamp or not expected_signature:
return False
if abs(int(time.time()) - timestamp) > TOLERANCE_SECONDS:
return False
body_str = raw_body.decode("utf-8")
signed_payload = f"{timestamp}.{body_str}".encode("utf-8")
computed_signature = hmac.new(
signing_secret.encode("utf-8"),
signed_payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed_signature, expected_signature)
Replay Protection
The timestamp is included in the signed payload, so a signature from an old delivery cannot be reused with a different body. Combined with the tolerance check (reject requests where t is more than 5 minutes from your server's clock), this protects against replay attacks.
Ensure your server's clock is reasonably accurate (NTP). If your tolerance is too large, replayed requests remain valid for longer.
Best Practices
- Always read the raw body before parsing JSON.
- Use a constant-time comparison (
crypto.timingSafeEqualin Node.js,hmac.compare_digestin Python). - Reject any request that lacks
X-Webhook-Signatureonce signing is enabled for your subscriber. - Store the signing secret in your secrets manager; never log it or commit it to source control.
- Use
event_idfor idempotency — duplicate deliveries are possible (see the Webhook Setup Guide).
For delivery behavior, retries, and endpoint requirements, see the Webhook Setup Guide.

