Skip to main content

Runtime verification (platform → merchant)

Outbound delivery (platform webhook delivery):
signedPayload = timestamp + "." + rawBody
signature     = HMAC_SHA256(webhook_secret, signedPayload)
header value  = "v1=" + hex(signature)
Delivery headers set by the platform:
HeaderValue
Content-Typeapplication/json
X-Stablecoin-EventEvent type string
X-Stablecoin-Event-IdPayload id field
X-Stablecoin-TimestampUnix seconds (integer as string)
X-Stablecoin-Signaturev1=<hex>
X-Stablecoin-Integration-IdPresent when invoice has integration_id

Runtime verification (WooCommerce plugin receiver)

The WooCommerce plugin (WooCommerce plugin webhook security module) verifies:
  1. X-Stablecoin-Signature and X-Stablecoin-Timestamp are present
  2. Timestamp within 300 seconds (MAX_AGE_SECONDS)
  3. X-Stablecoin-Integration-Id matches local integration ID when both are set
  4. Signature matches v1 format: v1= + hash_hmac('sha256', $timestamp . '.' . $raw_body, $secret)
  5. Legacy fallback accepted: sha256= + hash_hmac('sha256', $raw_body, $secret)

Verification steps (custom integrations)

  1. Read raw request body (before JSON parsing)
  2. Extract X-Stablecoin-Timestamp and X-Stablecoin-Signature
  3. Reject if timestamp is older than 300 seconds
  4. Compute v1= + HMAC-SHA256(secret, ${timestamp}.${rawBody})
  5. Compare using constant-time comparison

Example (Node.js)

const crypto = require("crypto");

function verifyWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-stablecoin-timestamp"];
  const signature = headers["x-stablecoin-signature"];
  if (!timestamp || !signature) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
  const signed = `${timestamp}.${rawBody}`;
  const expected = "v1=" + crypto.createHmac("sha256", secret).update(signed).digest("hex");
  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  } catch {
    return false;
  }
}