Webhooks
Signed POSTs for spending-limit steps, node lifecycle and billing events, with retries and a test button.
Events#
| Event | When |
|---|---|
spend_cap.step | The ladder moves up or down (from, to, forecast_cents, spent_cents) |
node.running / node.failed / node.stopped | Node lifecycle on Cloud and BYOC |
project.state | active ↔ limited ↔ suspended |
billing.payment_failed / billing.invoice | Billing events for the organisation |
Subscribe per project under Webhooks; HTTPS only. The event list is also returned by GET /v1/projects/{id}/webhooks.
Payload#
{
"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.
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
idcan arrive twice. Store processed ids for a day and skip repeats. - Ordering is not guaranteed across endpoints or after retries. For
spend_cap.stepusedata.toas the current state rather than replaying transitions; for nodes, fetchGET /v1/projects/{id}/nodesif you need the exact current list. - Fast ack. Return
2xximmediately 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
failingin 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#
| Event | Typical reaction |
|---|---|
spend_cap.step to 1–2 | post to your ops channel; nothing is wrong, the forecast crossed the limit |
spend_cap.step to 3–4 | raise the limit from the console or PATCH /v1/projects/{id}/limit, or accept refused joins for the rest of the month |
node.failed | nothing — autoscale replaces it; alert if it repeats |
node.running after a node.stopped | autoscale added capacity; good moment to check the TURN share |
project.state → suspended | payment failed past the grace period; new joins are refused until the card is updated |
billing.invoice | fetch 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.