quotastack Docs
Docs / API / Events / credit.low_balance

credit.low_balance

Fired once when a customer's effective balance (balance − reserved) crosses below the configured low_balance threshold while remaining > 0. Re-arms on grant / reservation release when effective recovers to or above threshold. Default tenant threshold is 0 (off). Exhausted cliff (high → ≤0) does not emit this event.

When it fires

QuotaStack sends this once, when a customer effective balance drops below your threshold and stays above zero. Effective balance is what a new charge can spend. So a big reservation can trip this with no debit at all.

When it does not fire

QuotaStack does not send this when the balance drops straight past zero. That case sends credit.exhausted, and only that one. QuotaStack sends nothing more while the balance stays low, however many charges land. The event arms again once a grant or a release lifts the balance back up. QuotaStack sends nothing at all while your threshold is 0. Every account starts there.

data
FieldTypeMeaning
before_effective_balancerequiredinteger (int64)

What the customer could spend before this change.

Unit: millicredits, where 1 credit is 1000 millicredits.

after_effective_balancerequiredinteger (int64)

What the customer can spend now. This number is below your threshold.

Unit: millicredits, where 1 credit is 1000 millicredits.

threshold_mcrequiredinteger (int64)

The threshold this customer crossed. Your account default, unless you set one on the customer.

Unit: millicredits, where 1 credit is 1000 millicredits.

triggerrequiredstring

What mutation caused the cross (e.g. consumption, reservation_create, expiry).

Example payload
{
  "event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
  "event_type": "credit.low_balance",
  "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:14:00Z",
  "idempotency_key": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
  "data": {
    "before_effective_balance": 6000,
    "after_effective_balance": 4000,
    "threshold_mc": 5000,
    "trigger": "consumption"
  }
}

What to do

Email the customer, or show a banner. Set the threshold high enough to leave them time to act.

Which calls fire this

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:

AttemptDelay after previous
1Immediate
230 seconds
35 minutes
430 minutes
52 hours
68 hours
724 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.