credit.expiring_soon
Fired once per credit block when that block's remaining balance will expire within the tenant's credit_expiring_soon_hours lead window (default 0 = off; recommended 72). Distinct from credit.expired (fires when the block actually expires) and from post-expiry credit.exhausted / credit.low_balance. Debounced per (customer, block_id); emission is best-effort.
When it fires
QuotaStack sends this before a credit block runs out, once it enters your lead window. One event covers one block. A customer holding three expiring packs gets three events. A QuotaStack job raises it. No API call does.
When it does not fire
QuotaStack sends nothing while credit_expiring_soon_hours is 0, which is where every account starts. QuotaStack does not send this twice for one block. A fresh grant makes a new block, so that one gets its own event.
| Field | Type | Meaning |
|---|---|---|
block_idrequired | string (uuid) | |
expiring_amountrequired | integer (int64) | Remaining millicredits on this block that will expire. |
expires_atrequired | string (date-time) | |
hours_remainingoptional | integer | Approximate whole hours until expires_at (ceil). |
lead_time_hoursoptional | integer | Tenant-configured lead window that matched this block. |
effective_balancerequired | integer (int64) | What the customer can spend today, before this block runs out. Unit: millicredits, where 1 credit is 1000 millicredits. |
spendable_after_expiryrequired | integer (int64) | max(0, effective_balance − expiring_amount) for this block. Multi-block same-window expiry may understate aggregate spendable. |
{
"event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
"event_type": "credit.expiring_soon",
"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-28T08:00:00Z",
"idempotency_key": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"data": {
"block_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
"expiring_amount": 10000,
"expires_at": "2026-07-31T08:00:00Z",
"hours_remaining": 72,
"lead_time_hours": 72,
"effective_balance": 10000,
"spendable_after_expiry": 0
}
} What to do
Warn the customer, or top them up, while the credits are still there. Read spendable_after_expiry to see what would be left. That figure counts this block alone, so it can read high when several blocks lapse together.
Which calls fire this
- QuotaStack's scheduler, on a timer — no API call involved.
See also
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:
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.
Loading…