Skip to content
Tollgate Docs

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

  1. Create a subscription in the dashboard at /settings/webhooks. Copy the signing secret — it is shown exactly once.
  2. Store the secret somewhere your consumer can read it (env var, secret manager).
  3. Verify the signature on every inbound delivery using @tollgatepay/core/webhooks.
  4. Return 2xx within 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

NameFired when
settlement.confirmedOn-chain settlement confirmed and matched against a USDC Transfer event.
settlement.failedSettlement attempt reverted or was abandoned after nonce reclaim.
blacklist.added_networkAgent added to the network-scope blacklist (OFAC match or escalation).
blacklist.added_developerYour account flagged an agent (developer-scope).
blacklist.removedA blacklist entry was cleared.
exposure.thresholdAn agent's unsettled exposure crossed a configured threshold.
iou.rejectedA signed IOU was rejected at ingestion (replay, blacklist, signature, etc).
iou.frozenAn IOU was frozen (Circle blacklist / compliance freeze).
kyc.state_changedYour KYC state transitioned (e.g. pending → verified).
api_key.revokedOne 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

HeaderMeaning
Tollgate-SignatureHMAC — t=<unix>,v1=<hex>. Sign over `${t}.${rawBody}`.
Tollgate-Event-IdUUID of the event. Idempotency key — consumers should dedup.
Tollgate-Event-TypeEvent type, e.g. `settlement.confirmed`.
Tollgate-Webhook-IdSubscription id that triggered this delivery.
Tollgate-Delivery-Attempt1-indexed retry counter.
traceparentW3C 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 dead and 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 dead immediately.
  • 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 t is 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.