🇪🇸 Español 🇬🇧 English 🇧🇷 Português

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.

Jul 11, 2026

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 (prefixed whsec_) 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-Timestamp is more than a few minutes old, reject the delivery.
  • Use X-Omnifox-Delivery-Id to 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_secret was generated/rotated, some legacy subscriptions are sent unsigned; rotate the secret from Settings > Webhooks to enable signing.
  • You lost the signing_secret: there's no way to retrieve it; rotate it with POST /api/v1/webhooks/{webhook}/rotate-secret and update your verification code with the new value.
Was this helpful?

Related articles