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
| Field | Notes |
|---|---|
block_id | The block that is about to expire. |
expiring_amount | Millicredits still on that block — what is at risk. |
expires_at | When it lapses. |
effective_balance | balance − reserved_balance − pending_balance at the moment the event fired. |
spendable_after_expiry | max(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_hours | Informational, omitted when zero. |
How it fits with the events you already have
| Event | When |
|---|---|
credit.expiring_soon | Before expires_at, once the block enters your lead window |
credit.expired | When the block actually expires — the accounting record |
credit.exhausted / credit.low_balance | After 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.
Loading…