---
title: "credit.low_balance"
description: "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](/docs/concepts/reservations) can trip this with no debit at all."
---

# 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](/docs/concepts/reservations) 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.

## Payload — `data`

| Field | Type | Required | Meaning |
|---|---|---|---|
| `before_effective_balance` | integer (int64) | required | What the customer could spend before this change. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `after_effective_balance` | integer (int64) | required | What the customer can spend now. This number is below your threshold. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `threshold_mc` | integer (int64) | required | The threshold this customer crossed. Your account default, unless you set one on the customer. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `trigger` | string | required | What mutation caused the cross (e.g. consumption, reservation_create, expiry). |

### Example payload

```json
{
  "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

- [`POST /v1/customers/{customer_id}/credits/adjust`](/docs/api/credits#adjustCustomerCredits)
- [`POST /v1/customer-by-external-id/{external_id}/credits/adjust`](/docs/api/credits#adjustCustomerCreditsByExternalId)
- [`POST /v1/reservations/{id}/commit`](/docs/api/reservations#commitReservation)
- [`POST /v1/entitlements/consume`](/docs/api/entitlements#consumeEntitlement)
- [`POST /v1/usage`](/docs/api/usage#recordUsage)
- [`POST /v1/usage/batch`](/docs/api/usage#recordUsageBatch)
- [`POST /v1/reservations`](/docs/api/reservations#reserveCredits)

## See also

- [Low balance alerts](/docs/cookbook/low-balance-alerts)
- [Credits](/docs/concepts/credits)

## 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:

```bash
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.
