// Platform

Budget Alerts

Define spending thresholds in cents. Get email or webhook notifications when project spend crosses each one. A threshold fires at most once per billing period; the monthly period reset clears notified_thresholds so each threshold can fire again next month.

Overview

Budget alerts monitor your project's monetary spend (computed from per-request cost on usage events, in cents) against configurable thresholds. Each threshold triggers only once per billing period.

All /billing/* routes are mounted on the dashboard router group and require an authenticated session plus a CSRF token. Obtain the token from the session bootstrap endpoint and supply it via the X-CSRF-Token header on every mutating request (POST, PUT, DELETE). Read-only GET requests still require the session cookie.

Threshold Lifecycle

A threshold transitions through three states each billing period: armed, fired, then reset on the month boundary.

API Endpoints

Method Endpoint Description
GET /billing/budgets List all budget alerts for the project.
POST /billing/budgets Create a new budget alert.
GET /billing/budgets/:budgetId Get budget alert details.
PUT /billing/budgets/:budgetId Update a budget alert.
DELETE /billing/budgets/:budgetId Delete a budget alert and its history. Returns 204 No Content with an empty body on success.
GET /billing/budgets/:budgetId/history Get alert history (query: ?limit=, default 50, max 100). Cursor pagination is not supported on this endpoint.
POST /billing/budgets/check Manually trigger a budget check for the project. See Budget Check.

Create a Budget Alert

Parameter Type Description
namerequired string Human-readable name for this budget alert.
budget_amountrequired integer Monthly budget limit in cents (e.g., 10000 = $100.00).
thresholdsoptional array of integer Percentage thresholds that trigger notifications (1-100). Defaults to [50, 75, 90, 100].
alert_channelsoptional array of string Notification channels: "email" and/or "webhook". Defaults to ["email"].
webhook_urloptional string HTTPS URL for webhook notifications. Required when alert_channels includes "webhook".
curl
curl -X POST https://xerotier.ai/billing/budgets \ -H "Authorization: Bearer xero_my-project_abc123" \ -H "Content-Type: application/json" \ -H "X-CSRF-Token: your-csrf-token" \ -d '{ "name": "Monthly API Budget", "budget_amount": 10000, "thresholds": [50, 75, 90, 100], "alert_channels": ["email", "webhook"], "webhook_url": "https://hooks.example.com/budget-alert" }'

The budget_amount is specified in cents. A value of 10000 represents $100.00.

Python
import requests headers = { "Authorization": "Bearer xero_my-project_abc123", "Content-Type": "application/json", "X-CSRF-Token": "your-csrf-token" } response = requests.post( "https://xerotier.ai/billing/budgets", headers=headers, json={ "name": "Monthly API Budget", "budget_amount": 10000, "thresholds": [50, 75, 90, 100], "alert_channels": ["email", "webhook"], "webhook_url": "https://hooks.example.com/budget-alert" } ) print(response.json())
Node.js
const response = await fetch( "https://xerotier.ai/billing/budgets", { method: "POST", headers: { "Authorization": "Bearer xero_my-project_abc123", "Content-Type": "application/json", "X-CSRF-Token": "your-csrf-token" }, body: JSON.stringify({ name: "Monthly API Budget", budget_amount: 10000, thresholds: [50, 75, 90, 100], alert_channels: ["email", "webhook"], webhook_url: "https://hooks.example.com/budget-alert" }) } ); const data = await response.json(); console.log(data);

Response Example

All response bodies use snake_case field names. The same shape is returned by list, get, create, and update endpoints.

JSON
{ "id": "00000000-1111-0000-1111-000000000000", "project_id": "00000000-2222-0000-2222-000000000000", "name": "Monthly API Budget", "budget_amount": 10000, "budget_amount_formatted": "$100.00", "thresholds": [50, 75, 90, 100], "alert_channels": ["email", "webhook"], "notified_thresholds": [], "current_spend": 0, "current_spend_formatted": "$0.00", "spend_percentage": 0.0, "remaining_budget": 10000, "remaining_budget_formatted": "$100.00", "period_start": "2026-04-01T00:00:00Z", "period_end": "2026-05-01T00:00:00Z", "is_enabled": true, "webhook_url": "https://hooks.example.com/budget-alert", "next_threshold": 50, "created_at": "2026-04-09T10:00:00Z", "updated_at": null }

Once every configured threshold has fired in the current period, next_threshold becomes null and notified_thresholds contains the full set:

JSON
{ "thresholds": [50, 75, 90, 100], "notified_thresholds": [50, 75, 90, 100], "next_threshold": null, "spend_percentage": 104.2 }

Response Fields

Field Type Description
id string (uuid) Budget alert identifier.
project_id string (uuid) Owning project identifier.
budget_amount integer Monthly budget limit in cents.
current_spend integer Spend for the current billing period in cents.
spend_percentage number Current spend as a percentage of the budget.
notified_thresholds array of integer Thresholds that have already fired in the current period.
next_threshold integer or null The next un-fired threshold, or null when all thresholds have fired.
period_start / period_end string (ISO 8601) Start (inclusive) and end (exclusive) of the current billing period.
is_enabled boolean Whether the alert is evaluated during budget checks.
webhook_url string or null Configured webhook URL, or null when webhook delivery is not in use.
created_at / updated_at string (ISO 8601) or null Timestamps; updated_at is null until the first update.

Update a Budget Alert

All fields are optional in an update request. Only provided fields are changed.

Parameter Type Description
nameoptional string Updated name.
budget_amountoptional integer Updated monthly budget limit in cents.
thresholdsoptional array of integer Updated threshold percentages.
alert_channelsoptional array of string Updated notification channels.
webhook_urloptional string Updated webhook URL (null to remove).
is_enabledoptional boolean Enable or disable the budget alert.
curl
curl -X PUT https://xerotier.ai/billing/budgets/budget-uuid \ -H "Authorization: Bearer xero_my-project_abc123" \ -H "Content-Type: application/json" \ -H "X-CSRF-Token: your-csrf-token" \ -d '{ "budget_amount": 20000, "is_enabled": true }'

Thresholds

A threshold is an integer percentage (1..100) of the budget. When spend crosses a threshold for the first time this billing period, the alert fires on every configured channel. Three common ladders:

Configuration Thresholds Use Case
Standard [50, 75, 90, 100] Gradual warnings as spending increases.
Conservative [25, 50, 75, 90, 100] Early warning for tight budgets.
Simple [80, 100] Alert only when nearing or at the limit.

A threshold fires once per billing period. After the 75% threshold fires it will not fire again until the next period resets notified_thresholds, even if spend drops below and re-crosses the line.

Notifications

Budget alerts support two notification channels:

Channel Description
email Email notification fanned out to every user with the owner role on the project (one or more recipients).
webhook HTTP POST to a configured URL. Requires HTTPS and a publicly routable host.

Webhook Payload

Budget-alert deliveries use a legacy non-enveloped payload shape distinct from the canonical event envelope used by other webhook events. All fields are snake_case.

When a threshold is triggered, the configured URL receives a POST request with this body:

JSON
{ "event_type": "budget.threshold_reached", "alert_id": "uuid", "alert_name": "Monthly API Budget", "project_id": "uuid", "project_name": "My Project", "threshold": 75, "current_spend_cents": 7500, "budget_amount_cents": 10000, "spend_percentage": 75.0, "timestamp": "2026-01-15T10:30:00Z" }

Webhook URL Rules

Webhook URLs are validated server-side. A URL that fails any rule below is rejected with HTTP 400 Webhook URL must not point to a private or loopback address (or Webhook URL must use HTTPS for the scheme check). Validation happens at create and update time, so a rejected URL never reaches the delivery worker.

Rule Rejected Values
Scheme Anything other than https.
Loopback 127.0.0.0/8, ::1.
Link-local 169.254.0.0/16.
RFC1918 private 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16.
Reserved hostnames localhost, localhost.localdomain, metadata.google.internal, 169.254.169.254.

Delivery Semantics

Delivery timeout is 30 seconds. Failed deliveries are retried by the background job queue; each attempt writes a row to alert history, so delivery_success = false entries may accumulate across retries until the delivery succeeds or the job is given up.

Alert History

Each triggered alert is recorded in the alert history with delivery status:

curl
curl https://xerotier.ai/billing/budgets/budget-uuid/history?limit=20 \ -H "Authorization: Bearer xero_my-project_abc123"
Python
import requests headers = {"Authorization": "Bearer xero_my-project_abc123"} response = requests.get( "https://xerotier.ai/billing/budgets/budget-uuid/history", headers=headers, params={"limit": 20} ) for record in response.json(): print(f"Threshold {record['threshold']}% at {record['notified_at']}")
Node.js
const response = await fetch( "https://xerotier.ai/billing/budgets/budget-uuid/history?limit=20", { headers: { "Authorization": "Bearer xero_my-project_abc123" } } ); const history = await response.json(); history.forEach(record => { console.log(`Threshold ${record.threshold}% at ${record.notified_at}`); });

History Record Fields

Field Description
threshold The percentage threshold that triggered (e.g., 75).
spend_at_alert Spend in cents when the alert triggered.
budget_at_alert Budget amount in cents when the alert triggered.
notified_at Timestamp of the notification (ISO 8601).
notification_channel Channel used (email or webhook).
delivery_success Whether the notification was delivered successfully.
error_message Error details if delivery failed (null on success).

Budget Check

POST /billing/budgets/check evaluates every enabled budget alert in the project against the current spend and fires any thresholds that are newly crossed. The route takes no request body.

JSON
{ "budgets_checked": 3, "alerts_triggered": 1, "triggered_alerts": [ { "budget_alert_id": "00000000-1111-0000-1111-000000000000", "name": "Monthly API Budget", "threshold": 75, "spend_percentage": 76.4, "notification_channels": ["email", "webhook"] } ] }

triggered_alerts is empty when no new threshold was crossed. Disabled alerts (is_enabled = false) are skipped; re-enabling an alert within the same period leaves any previously fired thresholds in notified_thresholds, so they will not fire again until the period resets.

Errors and Authentication

All /billing/budgets routes return the standard error envelope used by other dashboard APIs. See Error Handling for the envelope shape and the full type taxonomy.

JSON
{ "error": { "message": "Thresholds must be between 1 and 100", "type": "invalid_request_error" } }
Status Condition
400 Validation failure: thresholds outside 1..100, missing required fields, non-HTTPS webhook URL, or webhook URL pointing to a private or loopback address.
401 No active session, or the CSRF token in X-CSRF-Token is missing or stale on a mutating request.
403 The requested budget alert exists but belongs to a different project than the current request context (Budget alert does not belong to this project).
404 No budget alert with the given id exists.

Sample Error Bodies

JSON, threshold out of range
{ "error": { "message": "Thresholds must be between 1 and 100", "type": "invalid_request_error" } }
JSON, blocked webhook host
{ "error": { "message": "Webhook URL must not point to a private or loopback address", "type": "invalid_request_error" } }
JSON, cross-project access
{ "error": { "message": "Budget alert does not belong to this project", "type": "forbidden" } }

Billing Periods

Budget alerts operate on monthly billing periods. A background job resets each alert when its period_start is strictly older than the start of the new month, so resets are best-effort background work rather than a real-time event at 00:00 UTC. At reset the system clears:

  • notified_thresholds, Cleared so all thresholds can fire again.
  • current_spend, Recomputed from usage events for the new period.
  • period_start / period_end, Updated to the new month boundary.

Current spend is calculated from the cost field on usage events for the current billing period. Use POST /billing/budgets/check to force an immediate recalculation between scheduled runs.

See Also

  • Billing and Subscriptions, project billing context that funds the budget under audit.
  • Webhooks, canonical event envelope used by every webhook other than budget alerts.
  • Error Handling, envelope shape and the type taxonomy used by every 4xx body on this page.
  • Usage, where the per-request cost on usage events comes from.