Skip to content

Verifying signatures

Every delivery carries a Ciqra-Signature header. Reject any request whose signature does not verify — your endpoint is public, and anyone can POST to it.

Ciqra-Signature: t=1758535200,v1=530e6f8f…4596
Part Meaning
t Unix time (seconds) when CIQRA signed the delivery.
v1 Lowercase hex of HMAC-SHA256(key = signing secret, message = t + "." + raw body).

The key is your installation’s signing secret (ciqra_ss_…) as UTF-8 bytes — the whole string, prefix included. The message is the timestamp, a literal ., and the raw request body exactly as received. Verify before parsing: re-serialising parsed JSON changes the bytes, and the signature will not match.

  1. Split the header on , and each part on the first =; take t and v1.
  2. Reject if t is more than 5 minutes from your clock — this stops replays of captured deliveries.
  3. Compute hex(HMAC-SHA256(secret, t + "." + rawBody)).
  4. Compare with v1 in constant time.
verify-signature.mjs
import { createHmac, timingSafeEqual } from "node:crypto";
// Verify a CIQRA webhook. rawBody: the request body exactly as received (string), before any JSON parsing.
export function verifyCiqra(rawBody, header, secret, toleranceSec = 300, nowSec = Date.now() / 1000) {
const parts = {};
for (const part of String(header).split(",")) {
const eq = part.indexOf("=");
if (eq > 0) parts[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
}
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(nowSec - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest("hex");
const given = parts.v1 ?? "";
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

The Node.js and Python code above is the exact file this site’s test suite runs against the vector below.

Check your implementation with this before connecting a real store.

Input Value
secret ciqra_ss_test
raw body {"orderId":"0f8e","status":"Paid"}
t 1758535200
signed message 1758535200.{"orderId":"0f8e","status":"Paid"}
header t=1758535200,v1=530e6f8fc05a8e47dd1befcbdf45fa89b16b4491362f5e7c0427b44965bb4596

This value is pinned by a test in the platform itself, so it cannot drift from what CIQRA actually sends. Remember to pass a fixed “now” when testing, or the 5-minute window will reject the 2025 timestamp.

Header Value
Ciqra-Event The event type, e.g. order.paid.
Ciqra-Event-Id Stable id of the event — use it to deduplicate.
Ciqra-Delivery Id of this delivery attempt series.
Ciqra-Webhook-Id The subscription id.
Ciqra-Event-Age-Seconds Seconds since the event happened; large after retries.
User-Agent CIQRA-Webhooks/1.0

These headers are not signed. Use them for routing, and trust only the signed body for data.