Skip to main content
Cresora Commerce
Webhooks

Signature Verification

Verify HMAC-SHA256 webhook signatures to authenticate Cresora deliveries.

Every webhook delivery is signed with HMAC-SHA256 using your endpoint's signing secret. Verify this signature before processing any event.

Headers

HeaderValue
X-Cresora-Signaturev1=<hex-encoded-hmac>
X-Cresora-TimestampUnix timestamp of delivery (seconds)

The v1= prefix is a scheme selector, not part of the digest. It is not included in the HMAC input, and it exists so a future scheme version can be introduced without breaking existing receivers.

Verification algorithm

  1. Build the signing payload: timestamp.raw_body
  2. Compute HMAC-SHA256(payload, signing_secret)
  3. Compare to the value in X-Cresora-Signature (constant-time comparison)
  4. Reject if timestamp is older than 5 minutes
Node.js
import crypto from "node:crypto";

function verifyWebhook(req, secret) {
  const timestamp = req.headers["x-cresora-timestamp"];
  const signature = req.headers["x-cresora-signature"];
  const body = req.rawBody; // raw bytes, not parsed JSON

  // Reject stale deliveries (replay protection)
  const ageSeconds = Math.floor(Date.now() / 1000) - Number(timestamp);
  if (ageSeconds > 300) throw new Error("Webhook timestamp too old");

  // HMAC over the raw body bytes — never over a re-serialized string
  const payload = Buffer.concat([Buffer.from(`${timestamp}.`), body]);
  const expected = Buffer.from(
    "v1=" + crypto.createHmac("sha256", secret).update(payload).digest("hex"),
  );
  const received = Buffer.from(String(signature ?? ""));

  // timingSafeEqual throws on inputs of different length, so a forged or
  // missing header must be rejected before it is compared — never crash.
  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    throw new Error("Webhook signature mismatch");
  }
}
Python
import hmac, hashlib, time

def verify_webhook(body: bytes, headers: dict, secret: str):
    timestamp = headers["X-Cresora-Timestamp"]
    signature = headers["X-Cresora-Signature"]

    age = int(time.time()) - int(timestamp)
    if age > 300:
        raise ValueError("Webhook timestamp too old")

    # HMAC over the raw body bytes — never decode and re-encode the body
    payload = timestamp.encode() + b"." + body
    expected = "v1=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise ValueError("Webhook signature mismatch")
🔒Security requirement

Always use constant-time comparison (timingSafeEqual / hmac.compare_digest). String equality (===) is vulnerable to timing attacks. In Node, check the lengths first: timingSafeEqual throws on inputs of different length, and a forged signature must be a clean rejection, not a crash.

Storing your signing secret

Store the signing secret in your secrets manager (not in code or environment files committed to source control). Rotate it in the Partner Portal if compromised.