Webhook Delivery Now Signs Payloads with HMAC-SHA256
Webhook Delivery Now Signs Payloads with HMAC-SHA256
Version: 1.0.47
Overview
Starting in v1.0.47, every outbound webhook delivery is cryptographically signed using HMAC-SHA256. The signature is included as an X-Webhook-Signature header, allowing receiving systems to verify that a delivery genuinely originated from the platform and that the payload has not been tampered with in transit.
Background
Previous versions of the webhook delivery system (src/inngest/functions/webhook-deliver.ts) posted payloads to registered endpoint URLs without attaching a signature. Although each webhook registration already stored a secret value in the webhooks table (src/db/schema.ts), that secret was never used during delivery. Third-party systems configured to reject unsigned or incorrectly signed webhook requests would silently drop all deliveries.
What Changed
In the post-webhook step of the delivery function:
- The serialised request body is hashed using HMAC-SHA256 with the webhook's stored secret (
delivery.webhookSecret). - The hex-encoded digest is attached as the
X-Webhook-Signatureheader on the outboundPOSTrequest. - No other delivery behaviour is changed — retries, timeouts, and payload structure remain the same.
Verifying Signatures on Your Endpoint
To validate an incoming webhook on your server, recompute the HMAC-SHA256 digest of the raw request body using the shared secret and compare it to the value in the X-Webhook-Signature header. Example in Node.js:
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, secret, signatureHeader) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // raw Buffer or string — do not parse JSON first
.digest('hex');
// Use a timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
Important: Always compute the HMAC over the raw, unparsed body bytes. Parsing the body as JSON before hashing may alter whitespace or key ordering and cause the comparison to fail.
Configuration
The shared secret is set once per webhook registration. There is no additional configuration required in v1.0.47 — the platform reads the existing secret column from the webhooks table automatically.
If you registered a webhook before v1.0.47 and did not set a secret at the time, consult the webhook management UI or API to rotate or add a secret. Deliveries for webhooks with an empty secret will send an empty X-Webhook-Signature header.
Security Recommendations
- Always verify the signature on your receiving endpoint before processing the payload.
- Use a timing-safe comparison (e.g.
crypto.timingSafeEqualin Node.js,hmac.compare_digestin Python) to prevent timing-based attacks. - Treat your webhook secret as a credential — rotate it if it is ever exposed.
- Consider rejecting requests that do not include an
X-Webhook-Signatureheader entirely.