quotastack Docs
Docs / Cookbook / Notifying Users Before the Wallet Runs Dry

Notifying Users Before the Wallet Runs Dry

Use credit.low_balance and credit.exhausted to warn customers early and react when they hit zero, without polling balances.

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:

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:

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

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

FieldUse it for
before_effective_balanceHow far they fell in one step — a large drop may deserve a different message
after_effective_balanceWhat is actually left; show it in the notification
threshold_mcWhich threshold tripped, if you vary it per customer
triggerWhat 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:

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 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, and trigger it from credit.exhausted.

  • Webhook events — the full event catalog and payloads
  • Credits — how effective balance and reservations relate
  • Reservations — why held credit counts against a threshold