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
- Go to your project in the dashboard
- Click Webhooks in the project header
- Enter your endpoint URL and select which events to receive
- 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"
}
}| Field | Type | Description |
|---|---|---|
event_id | string | ID of the event in noburn |
project_id | string | Project the call belonged to |
model | string | Model that was requested |
cost_usd | number | Estimated cost of the blocked call |
block_reason | string | null | Why the call was blocked |
end_user_id | string | null | End 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
}
}| Field | Type | Description |
|---|---|---|
project_id | string | Project that hit its cap |
spend_usd | number | Total spend at time of exhaustion |
budget_cap_usd | number | The 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
}
}| Field | Type | Description |
|---|---|---|
project_id | string | Project that hit the threshold |
threshold_pct | number | The alert threshold percentage (e.g., 80) |
spend_usd | number | Current spend at time of alert |
threshold_usd | number | Dollar 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
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Timeout | 10 seconds |
| Retries | 3 attempts with exponential backoff (5s, 25s, 125s) |
| Max payload | 1 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);
});