Webhooks
Subscribe to macro events (settlement state, blacklist changes, KYC, API-key revocations) as signed HTTP POSTs. Tollgate delivers each event at least once with exponential backoff. Consumers verify the Tollgate-Signature header with the subscription secret.
Getting started
- Create a subscription in the dashboard at /settings/webhooks. Copy the signing secret — it is shown exactly once.
- Store the secret somewhere your consumer can read it (env var, secret manager).
- Verify the signature on every inbound delivery using
@tollgatepay/core/webhooks. - Return
2xxwithin 10 seconds. Non-2xx triggers a retry per the policy below.
Signature scheme
Each delivery carries an Tollgate-Signature header:
Tollgate-Signature: t=<unix_seconds>,v1=<hex-digest>hex-digest = hex(HMAC-SHA256(secret, `$${t}.${rawBody}`)). The timestamp is part of the signed material so a replayer cannot swap t forward without invalidating the signature. Reject deliveries where t is older than 5 minutes.
Using the verifier
The verifier ships in @tollgatepay/core/webhooks — Workers-safe (WebCrypto, no Node dependencies). Import directly:
import { verifyWebhookSignature } from "@tollgatepay/core/webhooks";
export async function POST(req: Request) {
const body = await req.text(); // read BEFORE JSON.parse
const header = req.headers.get("tollgate-signature");
const result = await verifyWebhookSignature({
header,
body,
secret: process.env.TOLLGATE_WEBHOOK_SECRET,
options: { maxAgeSeconds: 300 }, // 5-minute replay window
});
if (!result.ok) {
return new Response("unauthorized", { status: 401 });
}
const event = JSON.parse(body);
// Idempotency — dedup on event.id (or the Tollgate-Event-Id header).
// ...
return new Response("ok", { status: 200 });
}Secret rotation
Rotate in the dashboard. The new secret is returned once; the old secret stops validating immediately. During a live rotation you can pass both secrets to verifyWebhookSignature:
options: { secrets: [process.env.NEW_SECRET, process.env.OLD_SECRET] }Event catalog
| Name | Fired when |
|---|---|
settlement.confirmed | On-chain settlement confirmed and matched against a USDC Transfer event. |
settlement.failed | Settlement attempt reverted or was abandoned after nonce reclaim. |
blacklist.added_network | Agent added to the network-scope blacklist (OFAC match or escalation). |
blacklist.added_developer | Your account flagged an agent (developer-scope). |
blacklist.removed | A blacklist entry was cleared. |
exposure.threshold | An agent's unsettled exposure crossed a configured threshold. |
iou.rejected | A signed IOU was rejected at ingestion (replay, blacklist, signature, etc). |
iou.frozen | An IOU was frozen (Circle blacklist / compliance freeze). |
kyc.state_changed | Your KYC state transitioned (e.g. pending → verified). |
api_key.revoked | One of your API keys was revoked. |
Payload shape
{
"id": "7c0d6f22-...-a58", // UUID; dedup on Tollgate-Event-Id
"type": "settlement.confirmed",
"version": "1",
"emittedAt": 1737020234,
"data": {
"txHash": "0xabc...",
"chainId": 8453,
"settlerAddress": "0x...",
"agentAddress": "0x...",
"developerAddress": "0x...",
"amountMicros": "1234567",
"blockNumber": 8429103,
"confirmedAtUnix": 1737020230
}
}id, type, version, and emittedAt are always present. data is an event-specific object — see events.ts for the exact TypeScript types.
Delivery headers
| Header | Meaning |
|---|---|
Tollgate-Signature | HMAC — t=<unix>,v1=<hex>. Sign over `${t}.${rawBody}`. |
Tollgate-Event-Id | UUID of the event. Idempotency key — consumers should dedup. |
Tollgate-Event-Type | Event type, e.g. `settlement.confirmed`. |
Tollgate-Webhook-Id | Subscription id that triggered this delivery. |
Tollgate-Delivery-Attempt | 1-indexed retry counter. |
traceparent | W3C trace-context header. Pass through to your logger for cross-hop correlation. |
Content-Type | `application/json`. |
User-Agent | `Tollgate-Webhooks/1.0`. |
Retry + auto-disable policy
- Max attempts: 8. After 8 failed attempts the delivery is marked
deadand appears in the dashboard for manual retry. - Backoff: exponential (×2) with ±25% jitter, capped at 1 hour.
- Retryable statuses: 5xx, 408, 425, 429 and transport/timeout failures.
- Terminal statuses: other 4xx + config errors (blocked scheme, blocked host, malformed URL). The delivery is marked
deadimmediately. - Auto-disable: after 10 consecutive failures the subscription is disabled. Re-enable from the dashboard after fixing the receiver.
Security
- HTTPS only. HTTP URLs are refused at subscription time.
- SSRF-safe. Outbound deliveries resolve the hostname and refuse private / loopback / link-local IPs (RFC1918, 169.254/16, ::1, fc00::/7, fe80::/10, cloud metadata endpoints). Redirects are refused. DNS-rebind attacks are blocked because any resolved answer must be public.
- Rate limit. 1000 deliveries per subscription per minute (token bucket). Excess deliveries re-enqueue, they are never dropped silently.
- Replay window. Reject deliveries where the signed
tis older than 5 minutes.
Idempotency
Deliveries are at-least-once. Dedup on event.id (also echoed in the Tollgate-Event-Id header). Each retry of the same event reuses the same id — persist the id after the first successful delivery and short-circuit on repeats.
Troubleshooting
Recent delivery attempts + truncated response bodies are visible in the dashboard at /settings/webhooks/<id>. Use the “Send test” button to fire a synthetic event of any subscribed type. Dead-lettered deliveries can be re-queued from the same page.