Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

TypeBehavior when triggered
SoftLimitSends a notification to the customer and fires a webhook event. Billing continues.
HardLimitBlocks 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:

  1. Fetches all active alerts for the customer
  2. Compares current_spend_nanos against each alert’s threshold_nanos
  3. For SoftLimit: fires a notification webhook if threshold crossed and not already triggered
  4. For HardLimit: returns an error that blocks the charge if threshold is met or exceeded
  5. 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

  1. Create a HardLimit alert at the customer’s budget ceiling
  2. Create a SoftLimit alert at 80% of the ceiling for advance warning
  3. Customer approaches limit → SoftLimit fires notification
  4. Customer hits limit → HardLimit blocks further charges
  5. Customer pays bill or requests increase → Admin resets the alert
  6. Billing resumes normally