credit.exhausted
Fired once when a customer's effective balance crosses from > 0 to ≤ 0 (consumption, reservation create/commit, or expiry). Always enabled. Effective may be negative when reserved holds remain after balance drops; that still counts as exhausted. Re-arms when effective recovers above 0. A high→≤0 one-shot emits exhausted only (not also low_balance).
When it fires
QuotaStack sends this when a customer effective balance falls to zero or below. Spending, a hold, and an expiry can each take it there. This event is always on, and no setting turns it off.
When it does not fire
QuotaStack does not send this again while the balance sits at or below zero. The event arms again once a grant or a release lifts it above zero. QuotaStack sends nothing when the balance only dips below your threshold. That case sends credit.low_balance.
| Field | Type | Meaning |
|---|---|---|
before_effective_balancerequired | integer (int64) | What the customer could spend before this change. Unit: millicredits, where 1 credit is 1000 millicredits. |
after_effective_balancerequired | integer (int64) | What the customer can spend now. This is 0 or less. Unit: millicredits, where 1 credit is 1000 millicredits. |
threshold_mcrequired | integer (int64) | Your low-balance threshold. QuotaStack sends it here too, though this event ignores it. Unit: millicredits, where 1 credit is 1000 millicredits. |
triggerrequired | string | What mutation caused the cross (e.g. consumption, reservation_create, expiry). |
{
"event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
"event_type": "credit.exhausted",
"tenant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a01",
"environment": "live",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"external_customer_id": "user_42",
"created_at": "2026-07-28T09:31:00Z",
"idempotency_key": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"data": {
"before_effective_balance": 2000,
"after_effective_balance": 0,
"threshold_mc": 5000,
"trigger": "consumption"
}
} What to do
Stop the customer, or move them to a paid plan. Effective balance can read below zero, when a hold outlives the credits behind it.
Which calls fire this
See also
Delivery
QuotaStack guarantees at-least-once delivery. An event may be delivered more than once if your endpoint returns a non-2xx response, the connection fails, or the request exceeds the delivery timeout.
Delivery timeout: 5 seconds per attempt. If your endpoint does not return a 2xx within 5 seconds, the attempt is treated as a failure and retried. Not configurable today.
One webhook URL per tenant. Multiple URLs and per-event routing are not supported. Configure the URL via the tenant config endpoint.
Retry schedule
If delivery fails (non-2xx response, timeout, or network error), QuotaStack retries with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failed attempts, the event is moved to a dead letter queue. Dead-lettered events are not lost — you can requeue them yourself, from the dashboard (Activity → Webhooks → Redeliver) or the API:
curl -X POST https://api.quotastack.io/v1/webhooks/events/{event_id}/redeliver \
-H "X-API-Key: $QS_KEY" \
-H "Idempotency-Key: redeliver:{event_id}"
Redelivery resets the event to pending with a fresh retry schedule (7
new attempts). The next attempt signs with your current secret —
useful when the event dead-lettered because of a secret rotation or an endpoint
outage you have since fixed. Only dead_letter events can be
redelivered; the call returns 409 for events in any other status.
Handling duplicates
Because delivery is at-least-once, your webhook handler should be idempotent. Use
the webhook-id header for deduplication — if you have already
processed an event with that ID, return 200 and skip processing.
Loading…