---
title: "Notifying Users Before the Wallet Runs Dry"
description: "Use credit.low_balance and credit.exhausted to warn customers early and react when they hit zero, without polling balances."
order: 9
---

# Notifying Users Before the Wallet Runs Dry

The worst way for a customer to learn they are out of credits is a failed call. QuotaStack emits two webhooks so you can get ahead of it: one warning shot while there is still balance left, and one hard signal when there is none.

You do not need to poll balances for this. Both events fire on the **crossing**, not on every debit.

## Step 1: Pick a threshold

The low-balance event is off until you set a threshold, because the right number is specific to your product. A useful starting point is *roughly a day of typical usage* — enough runway for someone to notice the email and act on it.

If a typical customer burns 5,000 millicredits a day, start there:

```bash
curl -X PATCH https://api.quotastack.io/v1/admin/tenants/{tenant_id}/config \
  -H "Cookie: qs_admin_session=..." \
  -H "Idempotency-Key: config-low-balance:{tenantId}" \
  -H "Content-Type: application/json" \
  -d '{"low_balance_threshold_mc": 5000}'
```

This is tenant config, so it needs an admin session — a product API key cannot change it.

### Overriding for specific customers

Heavy customers burn a day of credit much faster, so one global number rarely fits everyone. Override per customer with your normal API key:

```bash
curl -X PATCH https://api.quotastack.io/v1/customers/{customer_id} \
  -H "X-API-Key: qs_live_..." \
  -H "Idempotency-Key: customer-low-balance:{customerId}" \
  -H "Content-Type: application/json" \
  -d '{"low_balance_threshold_mc": 50000}'
```

Set it to `null` to go back to inheriting the tenant default, or `0` to switch the warning off for that customer alone — useful for an enterprise account that is invoiced separately and should never see a top-up prompt.

## Step 2: Handle the two events

```js
app.post('/webhooks/quotastack', async (req, res) => {
  // Verify the HMAC signature first — see the Webhooks concept page.
  const event = req.body;
  res.sendStatus(200);            // ack fast, then work

  const { customer_id, external_customer_id, data } = event;

  switch (event.event_type) {
    case 'credit.low_balance':
      await notifyRunningLow(external_customer_id, {
        remaining: data.after_effective_balance,
        threshold: data.threshold_mc,
      });
      break;

    case 'credit.exhausted':
      await showTopUpPaywall(external_customer_id);
      break;
  }
});
```

Ack before you do the work. A slow handler causes a timeout, which causes a retry, which causes a duplicate notification.

## Step 3: Do not spam

Both events fire **once per episode**. After `credit.low_balance` fires, further debits below the threshold stay quiet — the customer gets one warning, not one per API call.

The event re-arms when balance recovers back over the line, whether from a top-up, a plan renewal, or a released reservation. So a customer who tops up and later drains again gets a fresh warning, which is what you want.

This means you do not need your own "already warned this user" flag. QuotaStack is already tracking the episode.

## What the payloads tell you

| Field | Use it for |
|---|---|
| `before_effective_balance` | How far they fell in one step — a large drop may deserve a different message |
| `after_effective_balance` | What is actually left; show it in the notification |
| `threshold_mc` | Which threshold tripped, if you vary it per customer |
| `trigger` | What caused it — `consumption`, `reservation_create`, `expiry`, and so on |

`trigger` is worth branching on. Running dry because usage burned the balance is a different customer conversation from running dry because a credit block expired.

## Reservations count

Both events track **effective** balance — `balance − reserved_balance − pending_balance`. A customer with 10,000 millicredits and an 8,000 hold on an in-flight job has 2,000 effective, and will trip a 5,000 threshold even though no ledger debit has happened yet.

This is deliberate. Effective balance is what they can actually spend right now, which is what a warning should be about. If the reservation is released without being committed, balance recovers and the event re-arms.

## One drop to zero sends one event

A customer who goes from comfortably above the threshold straight to zero in a single call gets `credit.exhausted` only — not a `low_balance` warning immediately followed by an exhausted alert. There is no useful window between the two, so QuotaStack does not manufacture one.

Design your handlers so `credit.exhausted` stands alone; never assume a `low_balance` arrived first.

## Getting ahead of expiry

Both events above are reactive: they fire once the balance has already moved. When the cause is a credit block expiring on a timer, you can be told in advance instead.

Set a lead window on tenant config and `credit.expiring_soon` fires that many hours before a block's `expires_at`, naming the block and how much is at risk:

```bash
curl -X PATCH https://api.quotastack.io/v1/admin/tenants/{tenant_id}/config \
  -H "Cookie: qs_admin_session=..." \
  -H "Idempotency-Key: config-expiring-soon:{tenantId}" \
  -H "Content-Type: application/json" \
  -d '{"credit_expiring_soon_hours": 72}'
```

It is off by default (`0`) and is per credit block rather than per customer, so it complements the two crossing events rather than replacing them. See [`credit.expiring_soon`](/docs/api/events/credit.expiring_soon) for the payload.

## What this does not do

QuotaStack sends webhooks. It does not send email, and it does not top anyone up automatically — those are your calls to make, which is why the events carry enough context to make them.

If you want the balance to refill on its own rather than paywalling, see [Auto-Buy From Wallet](/docs/cookbook/auto-buy-from-wallet), and trigger it from `credit.exhausted`.

## Related

- [Webhook events](/docs/api/events) — the full event catalog and payloads
- [Credits](/docs/concepts/credits) — how effective balance and reservations relate
- [Reservations](/docs/concepts/reservations) — why held credit counts against a threshold
