Webhooks
Receive real-time HTTP notifications when payment events occur, policy violations, budget thresholds, confirmed payments, and wallet funding.
Setting up webhooks
Webhook endpoints are configured in the dashboard at app.fixerprotocol.org → Settings → Webhooks, or via the API:
curl -X POST https://api.fixerprotocol.org/v1/webhooks \
-H "Authorization: Bearer $FIXER_API_KEY" \
-H "X-Agent-Id: my-agent" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/fixer",
"events": [
"payment.confirmed",
"policy.violated",
"budget.threshold",
"wallet.low_balance"
],
"secret": "your_webhook_signing_secret"
}'
Fixer Protocol sends a POST request to your URL for each matching event. Your endpoint must respond with HTTP 200 within 10 seconds.
Event types
payment.confirmed
Fired when a payment settles on Solana with confirmed status.
{
"id": "evt_01HZ9QP...",
"type": "payment.confirmed",
"created": "2026-05-30T14:22:03Z",
"agentId": "my-agent",
"data": {
"txHash": "5xGh9KmBv...",
"endpoint": "https://api.dune.com/api/v1/query/3326266/results",
"amount": { "usdc": 0.002 },
"protocol": "x402",
"solanaTxLink": "https://explorer.solana.com/tx/5xGh9KmBv..."
}
}
policy.violated
Fired when a payment is blocked by a spend policy. The violation is also recorded on-chain.
{
"id": "evt_01HZ9QR...",
"type": "policy.violated",
"created": "2026-05-30T14:22:47Z",
"agentId": "my-agent",
"data": {
"rule": "daily_budget",
"attempted_endpoint": "https://api.proprietary.com/v1/data",
"attempted_amount": { "usdc": 1.50 },
"policy_limit": { "usdc": 100 },
"current_spend": { "usdc": 99.80 },
"message": "Payment would exceed daily budget of 100 USDC. Current spend: 99.80 USDC."
}
}
budget.threshold
Fired when daily spend crosses a configured percentage of the daily budget. Configure thresholds at 50%, 75%, and 90% in dashboard settings.
{
"id": "evt_01HZ9QS...",
"type": "budget.threshold",
"created": "2026-05-30T13:55:10Z",
"agentId": "my-agent",
"data": {
"threshold_pct": 75,
"daily_budget": { "usdc": 100 },
"current_spend": { "usdc": 75.20 },
"remaining": { "usdc": 24.80 }
}
}
rate_limit.hit
Fired when a payment is rejected due to the rate limit policy.
{
"id": "evt_01HZ9QT...",
"type": "rate_limit.hit",
"created": "2026-05-30T14:01:00Z",
"agentId": "my-agent",
"data": {
"limit": { "calls": 500, "window": "1h" },
"calls_in_window": 500,
"window_resets_at": "2026-05-30T15:00:00Z"
}
}
wallet.low_balance
Fired when the wallet USDC balance drops below a configured threshold. Configure the threshold in dashboard settings (default: $5 USDC).
{
"id": "evt_01HZ9QU...",
"type": "wallet.low_balance",
"created": "2026-05-30T14:30:00Z",
"agentId": "my-agent",
"data": {
"balance": { "usdc": 3.42 },
"threshold": { "usdc": 5.00 },
"wallet_address": "7xKXtg2eH9sZ7pWb..."
}
}
wallet.funded
Fired when a wallet funding transaction confirms on Solana.
{
"id": "evt_01HZ9QV...",
"type": "wallet.funded",
"created": "2026-05-30T10:00:05Z",
"agentId": "my-agent",
"data": {
"txHash": "7KLMnPqR2sTuWx...",
"amount": { "usdc": 100 },
"new_balance": { "usdc": 142.50 }
}
}
All event types
| Event | Trigger |
|---|---|
payment.confirmed | Payment settles on Solana |
payment.failed | Payment fails after submission (network error, Solana rejection) |
policy.violated | Payment blocked by a spend policy rule |
budget.threshold | Daily spend crosses 50%, 75%, or 90% of daily budget |
rate_limit.hit | Payment rejected by rate limit policy |
wallet.low_balance | Wallet balance drops below configured threshold |
wallet.funded | Wallet funding transaction confirmed |
Signature verification
Every webhook request includes a Fixer-Signature header, an HMAC-SHA256 signature of the raw request body using your webhook secret. Always verify this before processing the event.
import { createHmac, timingSafeEqual } from "crypto";
const WEBHOOK_SECRET = process.env.FIXER_WEBHOOK_SECRET!;
function verifyWebhook(
rawBody: string,
signature: string | null,
): boolean {
if (!signature) return false;
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
// Use timing-safe comparison to prevent timing attacks
return timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex"),
);
}
// Express handler example
app.post("/webhooks/fixer", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["fixer-signature"] as string;
const body = req.body.toString();
if (!verifyWebhook(body, sig)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(body);
switch (event.type) {
case "policy.violated":
console.warn("Policy violation:", event.data.message);
break;
case "budget.threshold":
console.warn(`Budget ${event.data.threshold_pct}% used`);
sendSlackAlert(event.data);
break;
case "wallet.low_balance":
triggerAutomaticTopUp(event.data.wallet_address);
break;
}
res.status(200).json({ received: true });
});
The Fixer-Signature header format:
Fixer-Signature: sha256=<hex-encoded-hmac>
Delivery guarantees
- At-least-once delivery. Fixer Protocol retries events up to 5 times with exponential backoff (10s, 30s, 2m, 10m, 1h) if your endpoint returns a non-
200response or times out. - Ordering. Events within the same agent are delivered in order of occurrence, but retries may cause out-of-order delivery across agents. Use the
createdtimestamp for ordering. - Idempotency. Your handler should be idempotent, the same event may be delivered more than once. Use the event
idfield to deduplicate. - Timeout. Your endpoint must respond within 10 seconds. For slow processing, respond immediately and process asynchronously (e.g. queue the event and process in the background).
Testing webhooks
Send a test event from the dashboard or via the API to verify your endpoint is working:
curl -X POST https://api.fixerprotocol.org/v1/webhooks/test \
-H "Authorization: Bearer $FIXER_API_KEY" \
-H "X-Agent-Id: my-agent" \
-H "Content-Type: application/json" \
-d '{
"webhookId": "wh_01HZ...",
"type": "payment.confirmed"
}'
For local development, use a tool like ngrok to expose your local server to the internet.