AI Setu Docs
Operations

Webhooks

Signed outbound events for budget alerts — payload schema, signature verification, and retry policy.

AI Setu can POST events to an HTTPS endpoint you own — today, budget alerts (see Budgets → Alerts). Register endpoints in the console under Settings → Notifications, or over GraphQL with createNotificationWebhook.

Registering an endpoint

FieldNotes
URLMust be https://. Private, loopback, link-local and other reserved addresses are rejected, both when you save the URL and again on every delivery. Redirects are not followed.
EventsOne or more of the events below.
ScopeAll workspaces (default), or one workspace — a workspace-scoped endpoint only receives events about budgets in that workspace.

Each organization can register up to 10 endpoints. When you create one you get a signing secret (whsec_…). It is shown once: store it securely. To get a new secret, delete the endpoint and create it again.

Use Send test (or testNotificationWebhook) to deliver a signed ping event to an endpoint. The result appears in its delivery log.

Events

typeWhen
budget.thresholdA budget's usage crossed one of its alert thresholds. Fires once per threshold per reset period.
budget.exhaustedA budget's usage reached its limit. Fires once per reset period.
pingTest event. Only sent on request; you can't subscribe to it.

Payload

Every request is a POST with a JSON body:

{
  "id": "evt_0b6f1c9e-6a3e-4c1f-9f55-2f1f0c1d9a77",
  "type": "budget.threshold",
  "created_at": "2026-10-20T12:01:00.000Z",
  "data": {
    "budget": {
      "id": "0192f3c4-…",
      "level": "workspace",
      "level_id": "0192f3c4-…",
      "scope_name": "Production",
      "type": "cost",
      "limit": 100000000,
      "current_usage": 81234567,
      "reset": "monthly",
      "period_start": "2026-10-01T00:00:00.000Z",
      "next_reset_at": "2026-11-01T00:00:00.000Z"
    },
    "threshold": { "value": 80000000, "percent": 80, "kind": "percent" },
    "usage_percent": 81.23
  }
}
  • id is the event id. It's the same for every endpoint that receives the event, so use it to deduplicate.
  • limit, current_usage and threshold.value are in the budget's unit: USD micros for cost budgets (1,000,000 = $1) and raw token counts for tokens budgets.
  • threshold.kind is percent or absolute. threshold.percent is the share of the limit the threshold represents.
  • next_reset_at is null for budgets that never reset.
  • A ping event's data is { "webhook_id": "…", "message": "…" }.

Headers

HeaderValue
X-AI-Setu-EventThe event type.
X-AI-Setu-DeliveryUnique id of this delivery (one event → one delivery per endpoint). It stays the same across retries.
X-AI-Setu-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256> — see below.
Content-Typeapplication/json

Verifying the signature

v1 is the hex-encoded HMAC-SHA256 of "<t>.<raw request body>" keyed with your signing secret. To verify:

  1. Read the raw body bytes. Do this before any JSON parsing, which can change whitespace.
  2. Recompute the HMAC and compare it to v1 in constant time.
  3. Reject the request if t is more than 5 minutes from your clock, to stop replays.
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyAiSetuSignature(
  secret: string,
  rawBody: string,
  header: string,
  toleranceSeconds = 300,
): boolean {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=', 2) as [string, string]),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t)) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;

  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? '', 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}

With Express, mount express.raw({ type: 'application/json' }) on the webhook route so req.body is the raw Buffer, then pass req.body.toString('utf8').

Delivery and retries

  • Respond with any 2xx within 10 seconds to acknowledge. Do slow work after you respond.
  • Any other status, a timeout, or a connection error counts as a failure. The delivery is retried 5 times with exponential backoff, about 30 s, 1 min, 2 min, 4 min and 8 min after each failure. After that it is marked failed.
  • Deliveries to a URL that now resolves to a private or reserved address fail immediately, with no retries.
  • Delivery is at least once. You may occasionally get the same event twice, so deduplicate on id.
  • Disabled endpoints receive nothing (except a manual ping). Deleting an endpoint also deletes its delivery log.

The console shows each endpoint's recent deliveries: status (pending, retrying, delivered, failed), attempt number, the HTTP status you returned, and the error, if any. Over GraphQL, use notificationDeliveries.

Permissions

Viewing endpoints and deliveries requires the settings: view permission. Creating, editing, testing and deleting endpoints requires settings: update. Every change is recorded in the audit log.

On this page