quotastack Docs
Docs / Changelog / 2026-07-26 · Proactive expiry warnings

2026-07-26 · Proactive expiry warnings

A third balance alert — credit.expiring_soon fires before a credit block reaches its expires_at, so you can top a customer up or warn them while the credits still exist. Off by default; you set the lead time in hours.

Proactive expiry warnings

Additive release, and a follow-up to balance alert webhooks from earlier the same day. That release told you a customer had run low. This one tells you before credits disappear on a timer.

Added — credit.expiring_soon

Fires when a credit block enters a lead window you configure, ahead of its expires_at. Until now the first signal that a pack had lapsed was credit.expired, which arrives when the credits are already gone. This is the same story one lead window earlier, while you can still do something about it.

Off by default. The tenant credit_expiring_soon_hours starts at 0, which disables the event.

# Lead time in hours (admin session — owner or admin role)
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}'

72 is the recommended value for packs and annual grants; 8760 — one year — is the maximum. The setting is tenant-wide, and there is no per-customer override in this release.

Per block, not per customer

Unlike credit.low_balance and credit.exhausted, which describe a customer’s overall position, this event describes one credit block. A customer holding three packs that all lapse inside the window gets three events, each naming its own block_id.

Each block fires once and then stays quiet until its expires_at has passed. A fresh grant is a new block, so it gets its own event.

Payload

FieldNotes
block_idThe block that is about to expire.
expiring_amountMillicredits still on that block — what is at risk.
expires_atWhen it lapses.
effective_balancebalance − reserved_balance − pending_balance at the moment the event fired.
spendable_after_expirymax(0, effective_balance − expiring_amount). Each event counts only its own block, so with several blocks lapsing together this can understate the eventual total.
hours_remaining, lead_time_hoursInformational, omitted when zero.

How it fits with the events you already have

EventWhen
credit.expiring_soonBefore expires_at, once the block enters your lead window
credit.expiredWhen the block actually expires — the accounting record
credit.exhausted / credit.low_balanceAfter effective balance crosses, including as a result of that expiry

Leaving credit_expiring_soon_hours at 0 keeps exactly the behaviour you have today: no new event, and credit.expired still fires on the actual lapse.

Not included

Emission is best-effort — it never fails the expiry sweep or a credit mutation, so a delivery problem cannot block a customer’s credits from expiring on schedule.

As with the other balance alerts, these are webhooks only. QuotaStack does not send email and does not automatically top anyone up. Per-customer lead windows and an overage-threshold event are not shipped.

See credit.expiring_soon for the full payload.