noburn.devdocs

Webhooks

Get notified in real time when budgets are hit or calls are blocked.

noburn sends HTTP POST requests to your endpoint when key events occur. Use webhooks to trigger your own logic — send Slack alerts, pause AI features, notify users, or log to your own systems.

Setting up a webhook

  1. Go to your project in the dashboard
  2. Click Webhooks in the project header
  3. Enter your endpoint URL and select which events to receive
  4. Save — noburn will immediately send a test ping to verify the endpoint

Your endpoint must return a 2xx status within 10 seconds or the delivery is marked failed.

Verifying webhook signatures

Every delivery includes an X-Noburn-Signature header — a HMAC-SHA256 hex digest of the raw request body, signed with your webhook's secret.

Always verify this signature before processing the payload.

import hmac
import hashlib
from fastapi import Request, HTTPException

async def verify_webhook(request: Request, secret: str) -> dict:
    signature = request.headers.get("X-Noburn-Signature")
    if not signature:
        raise HTTPException(status_code=401, detail="Missing signature")

    body = await request.body()
    expected = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(f"sha256={expected}", signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    return await request.json()
import { createHmac, timingSafeEqual } from 'crypto';

export function verifyWebhook(
  rawBody: string,
  signature: string,
  secret: string,
): boolean {
  const expected = `sha256=${createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')}`;

  return timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected),
  );
}

// Express example
app.post('/webhooks/noburn', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-noburn-signature'] as string;
  if (!verifyWebhook(req.body.toString(), sig, process.env.NOBURN_WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  // handle event...
  res.sendStatus(200);
});
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "net/http"
)

func verifyWebhook(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expected))
}

Event types

call.blocked

Fired when a call is intercepted and blocked before reaching the LLM.

{
  "event": "call.blocked",
  "timestamp": "2025-01-15T14:32:01.234Z",
  "data": {
    "event_id": "evt_abc123",
    "project_id": "550e8400-e29b-41d4-a716-446655440000",
    "model": "gpt-4o",
    "cost_usd": 0.00848,
    "block_reason": "budget_exceeded",
    "end_user_id": "user_alice"
  }
}
FieldTypeDescription
event_idstringID of the event in noburn
project_idstringProject the call belonged to
modelstringModel that was requested
cost_usdnumberEstimated cost of the blocked call
block_reasonstring | nullWhy the call was blocked
end_user_idstring | nullEnd user identifier if provided

Use this to:

  • Send a push notification to the end user
  • Log blocked calls to your own analytics
  • Trigger a fallback to a cheaper model

budget.exhausted

Fired when a project's monthly budget cap reaches $0 remaining.

{
  "event": "budget.exhausted",
  "timestamp": "2025-01-15T18:00:45.000Z",
  "data": {
    "project_id": "550e8400-e29b-41d4-a716-446655440000",
    "spend_usd": 10.00,
    "budget_cap_usd": 10.00
  }
}
FieldTypeDescription
project_idstringProject that hit its cap
spend_usdnumberTotal spend at time of exhaustion
budget_cap_usdnumberThe budget cap that was reached

Use this to:

  • Page your on-call engineer
  • Disable AI features in your product UI
  • Auto-upgrade the budget cap

budget.alert

Fired when spend crosses a configured threshold percentage. Set thresholds in the dashboard under Alert Rules (e.g., "alert me at 50%, 80%, 100%").

{
  "event": "budget.alert",
  "timestamp": "2025-01-15T16:30:00.000Z",
  "data": {
    "project_id": "550e8400-e29b-41d4-a716-446655440000",
    "threshold_pct": 80,
    "spend_usd": 8.00,
    "threshold_usd": 8.00
  }
}
FieldTypeDescription
project_idstringProject that hit the threshold
threshold_pctnumberThe alert threshold percentage (e.g., 80)
spend_usdnumberCurrent spend at time of alert
threshold_usdnumberDollar amount the threshold represents

Use this to:

  • Send a Slack message to your team
  • Email the account owner
  • Trigger a budget review workflow

Delivery behavior

PropertyValue
MethodPOST
Content-Typeapplication/json
Timeout10 seconds
Retries3 attempts with exponential backoff (5s, 25s, 125s)
Max payload1 MB

A delivery is considered successful if your endpoint returns any 2xx status code. 4xx responses are not retried. 5xx and timeouts are retried up to 3 times.

Delivery logs

Each delivery attempt is recorded. View them in the dashboard under Webhooks → Delivery History. You can inspect the request, response status, and retry history for every event.

Common patterns

Slack alert on budget exhaustion

app.post('/webhooks/noburn', async (req, res) => {
  const event = req.body;

  if (event.event === 'budget.exhausted') {
    await fetch(process.env.SLACK_WEBHOOK_URL!, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `🚨 *Budget exhausted* for project \`${event.data.project_id}\`\n` +
              `Spent: $${event.data.spend_usd.toFixed(2)} / $${event.data.budget_cap_usd.toFixed(2)}`,
      }),
    });
  }

  res.sendStatus(200);
});

Disable AI features when budget is exceeded

// In your feature flag store
app.post('/webhooks/noburn', async (req, res) => {
  const event = req.body;

  if (event.event === 'budget.exhausted') {
    await featureFlags.disable('ai_chat', {
      projectId: event.data.project_id,
      reason: 'budget_exhausted',
    });
  }

  res.sendStatus(200);
});

On this page