Skip to content

Verify webhook signatures

Verify the exact timestamp-and-body HMAC contract in Node.js, Python, and PHP.

Last updated

Quire signs each attempt with HMAC-SHA256. Read the raw request body bytes before JSON parsing, then compute:

timestamp + "." + rawBody

Compare the hexadecimal digest to X-Quire-Signature-256 after its sha256= prefix using a constant-time comparison. Use X-Quire-Timestamp as the timestamp string. Reject stale timestamps according to your risk tolerance to reduce replay risk.

Headers

Every attempt sends:

  • X-Quire-Event
  • X-Quire-Event-Id
  • X-Quire-Delivery-Id
  • X-Quire-Attempt
  • X-Quire-Timestamp
  • X-Quire-Signature-256

Node.js and TypeScript

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyQuire(
  rawBody: Buffer,
  timestamp: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const expected = createHmac("sha256", secret)
    .update(timestamp)
    .update(".")
    .update(rawBody)
    .digest();
  const providedHex = signatureHeader.replace(/^sha256=/, "");
  if (!/^[a-f0-9]{64}$/i.test(providedHex)) return false;
  return timingSafeEqual(expected, Buffer.from(providedHex, "hex"));
}

Python

import hashlib
import hmac

def verify_quire(raw_body: bytes, timestamp: str, header: str, secret: str) -> bool:
    signed = timestamp.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    provided = header.removeprefix("sha256=")
    return hmac.compare_digest(expected, provided)

PHP

<?php

function verifyQuire(string $rawBody, string $timestamp, string $header, string $secret): bool
{
    $expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);
    $provided = preg_replace('/^sha256=/', '', $header) ?? '';

    return hash_equals($expected, $provided);
}

Re-encoding parsed JSON changes bytes and breaks verification. Capture the framework's raw body exactly as received.