How to verify ActiveQ webhook signatures

ActiveQ signs every webhook delivery with HMAC-SHA256, so your endpoint can prove a request came from ActiveQ and reject old or altered ones. Verify the signature over the raw body, check the timestamp, and ignore events you have already handled.

The scheme

Each delivery is an HTTPS POST with a JSON body and these headers.

Headers on every webhook delivery
HeaderValue
ActiveQ-Signaturet=<unix seconds>,v1=<hex>[,v1=<hex>]. During a secret rotation overlap there are two v1 values, newest secret first.
ActiveQ-Event-IdUnique id of the event. Use it to ignore a delivery you have already handled.
ActiveQ-Event-TypeThe event type, for example ticket.created.
ActiveQ-Delivery-IdId of this delivery. It stays the same across retries of it.
ActiveQ-Attempt1 for the first attempt, then 2, 3 and so on.
User-AgentActiveQ-Webhooks/1

The signature is the hex form of HMAC-SHA256, keyed with your endpoint’s signing secret, over the string <t>.<raw body>: the timestamp, a dot, then the body exactly as it was received. The signing secret is shown once when you add the endpoint or roll the secret, so copy it then. Each endpoint has its own secret.

Steps to verify a delivery

  1. Read the raw body. Use the bytes as received. If your framework parses the JSON first, turn that off for this route, or capture the raw body. Re-serialised JSON has different bytes, and the signature will not match.
  2. Parse the header. Take t and every v1 value. Reject the request if there is no usable timestamp.
  3. Check the timestamp. Reject the delivery if t is more than 300 seconds from the current time, in either direction. This is the replay window: a captured request cannot be sent again later.
  4. Compute the expected signature from your secret, t and the raw body.
  5. Compare in constant time against every v1 value, and accept the delivery if any one matches. Two values are sent while you rotate a secret.
  6. Dedupe on the event id. Deliveries are at least once and are not ordered, so the same event can arrive twice. Store the ActiveQ-Event-Id values you have handled and skip repeats.
  7. Answer with a 2xx status quickly and do the work afterwards. Any 2xx counts as delivered.

Node.js sample

This is the verification function from ActiveQ’s developer documentation. It needs Node 18 or later and no dependencies.

import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a Buffer or string), never re-serialized JSON.
export function verify(rawBody, signatureHeader, secret, nowSeconds = Math.floor(Date.now() / 1000)) {
  const parts = signatureHeader.split(",").map((p) => p.trim());
  const t = parts.find((p) => p.startsWith("t="))?.slice(2) ?? "";
  const candidates = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  if (!/^[0-9]{1,12}$/.test(t) || Math.abs(nowSeconds - Number(t)) > 300) return false; // not plain digits, too old or too new
  const expected = createHmac("sha256", secret).update(t + "." + rawBody).digest();
  let ok = false;
  for (const c of candidates) {
    if (!/^[0-9a-f]{64}$/.test(c)) continue; // exactly 64 lowercase hex characters
    if (timingSafeEqual(Buffer.from(c, "hex"), expected)) ok = true; // check every candidate
  }
  return ok;
}

Python sample

The same check in Python 3.10 or later, using only the standard library.

import hashlib, hmac, re, time
def verify(raw_body: bytes, signature_header: str, secret: str, now: float | None = None) -> bool:
    now = time.time() if now is None else now
    parts = [p.strip() for p in signature_header.split(",")]
    t = next((p[2:] for p in parts if p.startswith("t=")), "")
    candidates = [p[3:] for p in parts if p.startswith("v1=")]
    if not re.fullmatch(r"[0-9]{1,12}", t) or abs(now - int(t)) > 300:
        return False  # not plain ASCII digits, too old or too new
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    ok = False
    for c in candidates:
        if re.fullmatch(r"[0-9a-f]{64}", c) and hmac.compare_digest(c, expected):
            ok = True  # check every candidate
    return ok

Both samples are the same code that appears on the API documentation page of your own workspace, at /api/v1/docs on your workspace address.

Common mistakes

  • Verifying a re-serialised body instead of the raw bytes.
  • Comparing signatures with a normal string comparison, which can leak timing information.
  • Skipping the timestamp check, which leaves the endpoint open to replayed requests.
  • Using the secret of a different endpoint. Each endpoint has its own.
  • Checking only the first v1 value during a rotation.
  • A server clock that has drifted, which makes valid deliveries look stale. Keep clocks synchronised.
  • Doing slow work before answering. ActiveQ stops waiting after 10 seconds and counts the attempt as failed.

Delivery behaviour to plan for

  • The address must be a public HTTPS address on port 443. Redirects are not followed.
  • A failed delivery is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours and 12 hours, about 22 hours in all.
  • An endpoint that keeps failing is switched off, and Settings shows it as off with the reason “Repeated failures”. You can resend any delivery for 30 days.
  • A body is capped at 256 KB. Above that, the object is left out, truncated is true, and the ids are kept so you can fetch the object from the API.
  • Under Settings, then API and webhooks, an endpoint has Send test and Roll secret actions. The delivery log shows each attempt.
One webhook delivery opened in ActiveQ: its three attempts (500, 502, then 200), a Resend button, the stored request headers and the start of the JSON body. Sample data.One webhook delivery opened in ActiveQ: its three attempts (500, 502, then 200), a Resend button, the stored request headers and the start of the JSON body. Sample data.
Illustrative sample data. One delivery opened from the log: its attempts, a Resend button, the stored request headers and the body.View full sizeView full size

Webhooks are part of the API and webhooks offering, and the security page describes how endpoints and secrets are protected. If something here does not match what you see, the about page has the contact details for asking.

New to the idea? Read what a webhook signature is first.

What stays on your side

The check happens in your code, on your server, and ActiveQ doesn’t verify signatures for you. Delivery is at least once and not ordered, so your endpoint must handle repeats. ActiveQ never sends to private addresses, plain HTTP addresses or other ports, and it doesn’t follow redirects. Verification samples are published for Node and Python; other languages follow the same steps. To connect ActiveQ to other apps, see the guide to connecting Zapier, Make and n8n.

Last updated

Try ActiveQ with your own team

Every plan starts with a 14-day trial with all features and no card required. Pricing is flat per group of agents. Sign-up and billing are for US businesses.