Console
Docs/Operate

Webhooks

Signed POSTs for spending-limit steps, node lifecycle and billing events, with retries and a test button.

Events#

EventWhen
spend_cap.stepThe ladder moves up or down (from, to, forecast_cents, spent_cents)
node.running / node.failed / node.stoppedNode lifecycle on Cloud and BYOC
project.stateactive ↔ limited ↔ suspended
billing.payment_failed / billing.invoiceBilling events for the organisation

Subscribe per project under Webhooks; HTTPS only. The event list is also returned by GET /v1/projects/{id}/webhooks.

Payload#

json
{
  "id": "evt_01J…",
  "type": "spend_cap.step",
  "created_at": "2026-10-21T18:05:00Z",
  "project_id": "00607b44-…",
  "data": { "from": 0, "to": 1, "forecast_cents": 10400, "spent_cents": 6100, "cap_cents": 10000 }
}

Delivery#

POST with content-type: application/json, 10-second timeout, retried with exponential back-off (30 s, 1 min, 2 min, … capped at 1 h) for up to 12 attempts, about 8 hours in total. Any 2xx is success. The last status and error are shown per endpoint in the console; Send test delivers a ping event right away.

Verify the signature#

Every request carries Aurix-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Reject if t is older than five minutes.

Node
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret: string, header: string, body: string) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const mac = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex');
  return timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
}

The secret is shown once when the endpoint is created; delete and re-create the endpoint to rotate it.

Handling events#

  • Idempotency. Deliveries are retried, so the same id can arrive twice. Store processed ids for a day and skip repeats.
  • Ordering is not guaranteed across endpoints or after retries. For spend_cap.step use data.to as the current state rather than replaying transitions; for nodes, fetch GET /v1/projects/{id}/nodes if you need the exact current list.
  • Fast ack. Return 2xx immediately and process asynchronously; the 10-second timeout counts as a failure and triggers the retry schedule.
  • Failure state. An endpoint whose recent deliveries failed is marked failing in the console and keeps receiving new events; nothing is paused automatically. The last 200 deliveries per endpoint can be inspected and re-sent from the console.

What to do with each event#

EventTypical reaction
spend_cap.step to 1–2post to your ops channel; nothing is wrong, the forecast crossed the limit
spend_cap.step to 3–4raise the limit from the console or PATCH /v1/projects/{id}/limit, or accept refused joins for the rest of the month
node.failednothing — autoscale replaces it; alert if it repeats
node.running after a node.stoppedautoscale added capacity; good moment to check the TURN share
project.state → suspendedpayment failed past the grace period; new joins are refused until the card is updated
billing.invoicefetch the PDF link from GET /v1/orgs/{id}/billing for your accounting

Testing locally#

Point an endpoint at a tunnel or a request bin, press Send test for a ping, then lower the spending limit below the current forecast for a minute to see a real spend_cap.step — raise it again and the reverse transition arrives at the next pass.