Verify and Delivery
Request your endpoint receives
POST /inbound HTTP/1.1
Content-Type: application/json; charset=utf-8
svix-id: 9f2c…(64 hex chars)
cm-message-id: [email protected]
svix-timestamp: 1791452467123
svix-signature: Zm9vYmFy…
| Header | Description |
|---|---|
svix-id | Lower-case hex SHA-256 identifying the message and recipient. Not a UUID. Stable across redeliveries, so use it as your idempotency key. |
cm-message-id | Canonicalised RFC Message-ID (no angle brackets). |
svix-timestamp | Unix time in milliseconds (Svix proper uses seconds). |
svix-signature | Base64 HMAC-SHA512, no v1, prefix. |
Verify the signature
signature = base64( HMAC_SHA512( key = UTF8(secret), data = svix-id + "." + svix-timestamp + "." + rawBodyBytes ) )
- The key is your Webhook Secret (Settings → Integration) used as raw UTF-8. Do not base64-decode it or strip a
whsec_prefix. - Sign the exact bytes received. Do not parse and re-serialise.
- This is the same scheme as outbound event webhooks, so one secret and one verifier cover both. A stock Svix library will reject it because of the millisecond timestamp.
const crypto = require('crypto');
function verify(secret, headers, rawBody /* Buffer */) {
const mac = crypto.createHmac('sha512', secret)
.update(`${headers['svix-id']}.${headers['svix-timestamp']}.`)
.update(rawBody)
.digest('base64');
const got = Buffer.from(headers['svix-signature'] || '');
const want = Buffer.from(mac);
return got.length === want.length && crypto.timingSafeEqual(got, want);
}
Also reject stale svix-timestamp values (for example older than 5 minutes) to limit replay.
Unsigned deliveries. If your account has no active webhook secret, requests are sent with only svix-id and cm-message-id. Create a secret to receive signed requests. If the secret cannot be read (transient error), the delivery is not made at all rather than sent unsigned.
Respond
Return any 2xx status. CM.com waits up to 3 seconds to connect and 10 seconds in total. Respond quickly and process the message asynchronously.
Failures and replay
Delivery is at-least-once. Deduplicate on svix-id.
- A timeout, connection error,
408,429or5xxis a transient failure. Any other non-2xx(including a3xx) and a rejected URL is a permanent failure. - There is no automatic retry. Both kinds of failure hold the message for replay by CM.com. The message stays stored and visible in the Email App.
- A replay only re-sends to the webhooks that did not succeed. A webhook that already returned
2xxfor the message is skipped for 24 hours, so replays do not normally duplicate deliveries to it.