Webhooks

Signature verification

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

Required verification inputs

  • Raw HTTP request body bytes (exact, unparsed).
  • x-xpend-signature from the delivery request.
  • x-xpend-timestamp from the delivery request.
  • x-xpend-delivery-id for logging and deduplication.
  • Endpoint signing secret returned at endpoint creation/rotation.

Node.js example

import crypto from "node:crypto";

function verifyXpendSignature(params: {
  rawBody: string;
  signatureHeader: string;
  timestamp: string;
  secret: string;
}) {
  const expected = crypto
    .createHmac("sha256", params.secret)
    .update(`${params.timestamp}.${params.rawBody}`, "utf8")
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(params.signatureHeader, "hex"),
  );
}

Best practices

  • Reject requests with missing or invalid x-xpend-signature / x-xpend-timestamp headers.
  • Keep one active secret per webhook endpoint in your secret manager.
  • Rotate with /v1/webhooks/endpoints/{endpointId}/rotate-secret.
  • During rotation rollout, deploy secret updates before accepting new deliveries.

Common failure mode

Parsing JSON before verification changes the body representation. Verify first using the raw body, then parse.