Verifying the HMAC signature of outgoing webhooks (receiver side)
How to validate on your own server that an Omnifox outgoing webhook is authentic, using its HMAC-SHA256 signature.
When you set up an outgoing Webhook in Omnifox (Settings > Webhooks), every event (contact.created, message.delivered, conversation.assigned, etc.) is sent as an HTTP POST to the URL you configured. So your server can confirm that POST really came from Omnifox and not a third party, every delivery includes an HMAC-SHA256 signature.
Requirements
- A Webhook created under
Settings > Webhooks, with an HTTPS URL (required). - The
signing_secret(prefixedwhsec_) that Omnifox shows you only once, when the webhook is created or when you rotate it (POST /api/v1/webhooks/{webhook}/rotate-secret). Store it securely right away — it can't be retrieved again.
Headers you receive on every delivery
| Header | Content |
|---|---|
X-Omnifox-Event |
Event name, e.g. message.delivered |
X-Omnifox-Delivery-Id |
Unique ID for that delivery (useful for de-duping retries) |
X-Omnifox-Timestamp |
Unix timestamp (seconds) when the payload was signed |
X-Omnifox-Signature |
sha256=<hex> — the HMAC signature |
X-Omnifox-Attempt |
Attempt number (1 = first attempt) |
How the signature is computed
signature = HMAC_SHA256( "{timestamp}.{exact_json_body}" , signing_secret )
The timestamp is the same value as the X-Omnifox-Timestamp header, joined with a dot (.) to the JSON body exactly as received (not reformatted or re-ordered).
Example: verification in Node.js
const crypto = require('crypto');
function verifyOmnifoxWebhook(rawBody, headers, signingSecret) {
const signatureHeader = headers['x-omnifox-signature'] || '';
const timestamp = headers['x-omnifox-timestamp'] || '';
const [, providedHex] = signatureHeader.split('=');
const expectedHex = crypto
.createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const valid = providedHex &&
crypto.timingSafeEqual(Buffer.from(expectedHex), Buffer.from(providedHex));
return valid;
}
Always use the raw body (unparsed string) to compute the signature; if your framework has already parsed the JSON, you need to capture the raw body before that happens.
Tips
- Reject (HTTP 401) any delivery whose signature doesn't match, before processing the payload.
- Add an optional replay check: if
X-Omnifox-Timestampis more than a few minutes old, reject the delivery. - Use
X-Omnifox-Delivery-Idto de-duplicate: Omnifox retries deliveries with exponential backoff if your endpoint returns an error or times out, so you may receive the same event more than once.
Troubleshooting
- The signature never matches: the most common cause is comparing against the JSON re-serialized by your framework instead of the exact raw body that arrived.
- No signature headers arrive at all: if the webhook was created before
signing_secretwas generated/rotated, some legacy subscriptions are sent unsigned; rotate the secret fromSettings > Webhooksto enable signing. - You lost the
signing_secret: there's no way to retrieve it; rotate it withPOST /api/v1/webhooks/{webhook}/rotate-secretand update your verification code with the new value.
Related articles
-
How to connect integrations and external apps
Connect Omnifox with your favorite tools from the integrations catalog in just a few steps.
-
Generate and use API keys and webhooks
Create API keys to access Omnifox programmatically and set up webhooks to receive real-time events.
-
Getting started with integrations (webhook / OAuth / API key)
Connect Omnifox with your other tools. Learn when to use a webhook, a gallery OAuth connection, or an API key.
-
The Omnifox REST API: introduction and plan requirements
What the Omnifox REST API is, which plan you need (Crece and up), how to authenticate with the X-API-Key header, and where the docs live.
-
Create a personal API key (PAT)
Learn how to generate your first personal API key in Omnifox to connect integrations, scripts, or external tools to your workspace.