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
| type | When |
|---|---|
| settlement.processed | Determination and artifacts were issued |
| settlement.review_required | Event 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
| Header | Meaning |
|---|---|
| x-fiscal402-timestamp | Unix seconds at send time |
| x-fiscal402-signature | sha256=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.