Webhooks

Signature verification

Always verify webhook signatures against the raw request body before processing events.

Verify before parsing

Always verify webhook signatures against the raw request body before processing events. Parsing JSON before verification changes the body representation. Verify first using the raw body, then parse.

Required verification inputs

  • X-Xpend-Signature, HMAC-SHA256 of timestamp + raw body.
  • X-Xpend-Timestamp, Unix seconds; reject requests older than 5 minutes.
  • Signing secret, stored in your secret manager, never logged.
  • Raw HTTP request body, exactly as received, no JSON round-trip.

Node.js example

import crypto from "node:crypto";

export function verifyXpendSignature({
  rawBody,
  signatureHeader,
  timestampHeader,
  secret,
  toleranceSeconds = 300,
}) {
  const ts = Number(timestampHeader);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) {
    throw new Error("timestamp_outside_tolerance");
  }

  const signedPayload = `${ts}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  const provided = Buffer.from(signatureHeader, "hex");
  const computed = Buffer.from(expected, "hex");
  if (
    provided.length !== computed.length ||
    !crypto.timingSafeEqual(provided, computed)
  ) {
    throw new Error("signature_mismatch");
  }
}

Best practices

  • Enable signature verification on raw body.
  • Reject deliveries older than your tolerance window, replay protection.
  • During rotation rollout, deploy secret updates before accepting new deliveries.
  • Keep one active secret per webhook endpoint in your secret manager.