Skip to content

Webhooks

API

Webhooks

Optional signed settlement.processed and settlement.review_required events. HMAC over timestamp.rawBody.

Fiscal402 can POST a signed envelope after a settlement is processed or held for review. Webhooks are optional. The HTTP ingest response already contains the event id.

Event names

typeWhen
settlement.processedDetermination and artifacts were issued
settlement.review_requiredEvent stored without an invented rate

Payload

settlement.processed.json

{
  "id": "x402-4f9c1670",
  "type": "settlement.processed",
  "createdAt": "2026-09-10T12:04:11.000Z",
  "data": {
    "settlementId": "x402-4f9c1670",
    "reviewStatus": "PROCESSED",
    "regime": "OSS_B2C",
    "taxCategoryCode": "S",
    "ratePercent": 19,
    "txHash": "0xYOUR_SETTLED_TX",
    "network": "eip155:1",
    "amountUsdc": "1.00",
    "fx": { "pair": "USDC/EUR", "netEur": "0.86", "vatEur": "0.16" },
    "ublPath": "/settlements/x402-4f9c1670/ubl"
  }
}

Verify HMAC

Sign timestamp.rawBody with the webhook secret. Compare using a constant-time equality check. Reject if the timestamp is too old.

verify-webhook.ts

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

export function verifyFiscal402Webhook(input: {
  secret: string;
  timestamp: string;
  rawBody: string;
  signature: string;
}) {
  const digest = createHmac("sha256", input.secret)
    .update(`${input.timestamp}.${input.rawBody}`)
    .digest("hex");
  const expected = Buffer.from(`sha256=${digest}`);
  const received = Buffer.from(input.signature);
  if (expected.length !== received.length) return false;
  return timingSafeEqual(expected, received);
}

Headers

HeaderMeaning
x-fiscal402-timestampUnix seconds at send time
x-fiscal402-signaturesha256=hex HMAC

Retries

Failed deliveries retry with exponential backoff, capped at fifteen minutes between attempts. Treat the settlement id as idempotent — the same event may be delivered more than once.