Spend Alerts
Spend alerts let you set thresholds on customer spending and take action when those thresholds are hit. There are two alert types: SoftLimit (notify) and HardLimit (block).
Alert Types
| Type | Behavior when triggered |
|---|---|
SoftLimit | Sends a notification to the customer and fires a webhook event. Billing continues. |
HardLimit | Blocks new usage charges until the alert is reset. Returns an error if the customer tries to incur new charges. |
Use Cases
- Spend notifications: Alert customers at 80% of their expected monthly spend
- Budget caps: Block usage for free-tier users beyond their quota
- Enterprise spend controls: Finance-set hard limits on department cost centers
- Anomaly detection: Alert when a customer’s spend spikes unexpectedly
Create a Spend Alert
# [curl]
curl -X POST https://api.bill.sh/admin/v1/customers/$CUSTOMER_ID/alerts \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"alert_type": "SoftLimit",
"threshold_nanos": "100000000000000"
}'
# [Python]
import requests
# Create a SoftLimit at $100
resp = requests.post(
f"https://api.bill.sh/admin/v1/customers/{customer_id}/alerts",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
json={
"alert_type": "SoftLimit",
"threshold_nanos": "100000000000000", # $100.00
},
)
alert = resp.json()
print(f"Alert {alert['id']} — {alert['alert_type']} at threshold {alert['threshold_nanos']}")
# Also set a HardLimit at $125 (25% buffer)
hard_resp = requests.post(
f"https://api.bill.sh/admin/v1/customers/{customer_id}/alerts",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
json={
"alert_type": "HardLimit",
"threshold_nanos": "125000000000000", # $125.00
},
)
hard_alert = hard_resp.json()
print(f"Hard limit set: {hard_alert['id']}")
// [Node.js]
// Create a SoftLimit at $100
const softResp = await fetch(
`https://api.bill.sh/admin/v1/customers/${customerId}/alerts`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
alert_type: "SoftLimit",
threshold_nanos: "100000000000000", // $100.00
}),
}
);
const softAlert = await softResp.json();
console.log(`SoftLimit alert: ${softAlert.id}`);
// Also set a HardLimit at $125
const hardResp = await fetch(
`https://api.bill.sh/admin/v1/customers/${customerId}/alerts`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${ADMIN_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
alert_type: "HardLimit",
threshold_nanos: "125000000000000", // $125.00
}),
}
);
const hardAlert = await hardResp.json();
console.log(`HardLimit alert: ${hardAlert.id}`);
// [Go]
// Create a SoftLimit at $100
body, _ := json.Marshal(map[string]string{
"alert_type": "SoftLimit",
"threshold_nanos": "100000000000000",
})
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/customers/"+customerID+"/alerts",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var alert map[string]interface{}
json.NewDecoder(resp.Body).Decode(&alert)
fmt.Printf("Alert %v — %v\n", alert["id"], alert["alert_type"])
threshold_nanos: 100000000000000 = $100.00 USD.
Response:
{
"id": "01944b1f-0000-7000-8000-000000000030",
"customer_id": "01944b1f-0000-7000-8000-000000000001",
"alert_type": "SoftLimit",
"threshold_nanos": "100000000000000",
"triggered": false,
"created_at": "2026-02-28T10:00:00Z"
}
List Alerts for a Customer
# [curl]
curl https://api.bill.sh/admin/v1/customers/$CUSTOMER_ID/alerts \
-H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
resp = requests.get(
f"https://api.bill.sh/admin/v1/customers/{customer_id}/alerts",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
alerts = resp.json()
for alert in alerts:
threshold_usd = int(alert["threshold_nanos"]) / 1e12
status = "TRIGGERED" if alert["triggered"] else "active"
print(f"{alert['alert_type']:10} ${threshold_usd:.2f} — {status}")
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/admin/v1/customers/${customerId}/alerts`,
{ headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` } }
);
const alerts = await resp.json();
for (const alert of alerts) {
const thresholdUsd = (BigInt(alert.threshold_nanos) / BigInt(1e12)).toString();
console.log(`${alert.alert_type}: $${thresholdUsd} — ${alert.triggered ? "TRIGGERED" : "active"}`);
}
// [Go]
req, _ := http.NewRequest("GET",
"https://api.bill.sh/admin/v1/customers/"+customerID+"/alerts", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var alerts []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&alerts)
for _, alert := range alerts {
fmt.Printf("%v — triggered: %v\n", alert["alert_type"], alert["triggered"])
}
How check_spend() Works
The platform calls check_spend(customer_id, current_spend_nanos) before every metered charge. The SpendAlertService:
- Fetches all active alerts for the customer
- Compares
current_spend_nanosagainst each alert’sthreshold_nanos - For
SoftLimit: fires a notification webhook if threshold crossed and not already triggered - For
HardLimit: returns an error that blocks the charge if threshold is met or exceeded - Marks the alert as
triggered = true
Reset an Alert
After investigating or when the customer has cleared their balance, reset the alert to allow billing to resume:
# [curl]
curl -X POST https://api.bill.sh/admin/v1/customers/$CUSTOMER_ID/alerts/$ALERT_ID/reset \
-H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
resp = requests.post(
f"https://api.bill.sh/admin/v1/customers/{customer_id}/alerts/{alert_id}/reset",
headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
print(resp.json()) # {"triggered": false, ...}
// [Node.js]
const resp = await fetch(
`https://api.bill.sh/admin/v1/customers/${customerId}/alerts/${alertId}/reset`,
{
method: "POST",
headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` },
}
);
const result = await resp.json();
console.log("Alert reset, triggered:", result.triggered); // false
// [Go]
req, _ := http.NewRequest("POST",
"https://api.bill.sh/admin/v1/customers/"+customerID+"/alerts/"+alertID+"/reset",
nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
This sets triggered = false and clears the blocked state for HardLimit alerts.
Typical Workflow
- Create a
HardLimitalert at the customer’s budget ceiling - Create a
SoftLimitalert at 80% of the ceiling for advance warning - Customer approaches limit →
SoftLimitfires notification - Customer hits limit →
HardLimitblocks further charges - Customer pays bill or requests increase → Admin resets the alert
- Billing resumes normally