Skip to main content
When a webhook has signing: true, every request Omni Z-API sends to your endpoint carries an HMAC-SHA256 signature. This page describes exactly what is signed, so you can reproduce the computation on your server. It applies to both webhook types — channel and template.

Headers you receive

  • t — the moment the request was signed, in seconds since the epoch (UTC).
  • v1 — the HMAC-SHA256 in lowercase hex. The v1= prefix is the scheme version and is not part of what gets signed.
x-idempotency-key is not optional if you verify the signature: it is the only source of topic, partition, and offset, which are part of the computation.

What gets signed

The signature is computed over a canonical string of five fields separated by \n (byte 0x0A, no \r, no trailing newline):
The body goes in literally, byte for byte as it was sent — no reserialization, no key reordering, no whitespace stripping.
Computing the HMAC over the body alone does not work. The body is only the last of the five fields.

If the body arrives compressed

Requests of 1024 bytes or more are sent with content-encoding: gzip. The signature is computed over the uncompressed bytes. If your HTTP framework decompresses the body for you, use the body it hands you. If you read the raw body, decompress it before computing the HMAC — otherwise verification passes on small payloads and fails on large ones, which looks like flakiness.

Complete example

Every value below is consistent with the others: with this secret and this body, the v1 in the header is reproducible. Secret returned by the API (example, not a real secret):
Request received:
The canonical string built from it — t comes from x-webhook-signature, while topic, partition, and offset come from x-idempotency-key:
Check it from the terminal:

Verifying on your server

Read the raw body. Frameworks that JSON.parse and then JSON.stringify before you sign change the bytes (key order, whitespace, escapes) and the signature will not match. In Express, use express.raw({ type: 'application/json' }) on the webhook route.

Replay protection

Compare t against your server’s current time and reject requests outside an acceptable window — 5 minutes is a reasonable value. That alone is not enough: also use x-idempotency-key to discard redeliveries of the same event, which arrive with the same value.

Rotating the secret

The secret is returned only in the create response or in an update response with signing: true. GET does not return it.
Every PATCH with signing: true generates a new secret and invalidates the previous one — including when signing was already enabled and you only meant to change another field in the same request. If you do not want to rotate, omit the signing field.
After rotating, verify with the secret from the most recent response you received.

When the signature does not match

  1. Are you signing all five fields, and not the body alone?
  2. Is the separator a bare \n, with no \r and no trailing newline?
  3. Is the body the raw one, with no reserialization by your framework?
  4. If content-encoding: gzip was present, did you decompress first?
  5. Did topic, partition, and offset come from the x-idempotency-key of this same request?
  6. Is the secret the one from the most recent response with signing: true?