Billing Overage in Arrears
Let customers keep working past their credit balance, then invoice the recorded overage each cycle through your own PSP. QuotaStack stays payment-agnostic.
Billing Overage in Arrears
Pattern: soft limits + record-and-report. Customers never hit a hard wall; you invoice what they overran at the end of the cycle.
New to overage? Overage explains the three policies, the overage record, and the read endpoint this recipe uses.
The goal
Run a postpaid (or soft-limit) model: a customer consumes past their granted credit, keeps working, and you invoice the overage after the fact — through your own payment processor. QuotaStack records the overage; you own the money movement.
How overage works
Under an allow or notify overage policy, a consume that exceeds spendable credit does not fail and does not push the balance negative. Instead QuotaStack:
- Drains whatever spendable credit exists, flooring the balance at zero.
- Records the uncovered remainder as an append-only overage event.
- Surfaces that amount inline on the consume response and rolls it into the next postpaid
subscription.renewedwebhook.
block is unchanged — it refuses usage beyond the spendable balance.
Step 1: Set the overage policy
Overage is opt-in. The tenant default is block. Set it to allow to bill in arrears:
curl -X PATCH https://api.quotastack.io/v1/tenants/{tenant_id}/config \
-H "X-API-Key: qs_live_..." \
-H "Idempotency-Key: config-overage:{tenant_id}" \
-H "Content-Type: application/json" \
-d '{ "overage_policy": "allow" }'
Use notify instead of allow to also flag the account for webhook notification. (A dedicated pre-invoice alert, credit.overage_threshold_exceeded, is planned but not yet shipped — today notify behaves like allow.)
The tenant setting is the default. Every customer without their own value uses it. A customer can set its own overage_policy field, and that value wins when set — see Overage policy. So arrears for some accounts and hard blocking for others is one PATCH call per customer.
Step 2: Let consume run past zero
Once spendable credit is exhausted, POST /v1/entitlements/consume still succeeds:
POST /v1/entitlements/consume
Idempotency-Key: consume:img:req_98f2
{ "customer_id": "user_abc", "billable_metric_key": "image_generation", "units": 3 }
Here the customer had 1 credit (1,000 mc) of spendable balance left and requested 3 credits’ worth of work. One credit is drained; the remaining two are recorded as overage:
{
"type": "metered",
"allowed": true,
"debited": true,
"reason": "overage_recorded",
"customer_id": "019d6258-07ba-7418-83be-58f5fde53e4e",
"billable_metric_key": "image_generation",
"units": 3,
"estimated_cost": 3000,
"balance": 0,
"reserved_balance": 0,
"pending_balance": 0,
"effective_balance": 0,
"overage": 2000,
"subscription_status": "active",
"overage_policy": "allow"
}
Key fields:
allowed: trueandreason: "overage_recorded"mean the request went through. This is informational, not an error.overage: 2000is the millicredits recorded for arrears billing — present only when overage accrued.balancestays at0. It never goes negative.
Step 3: Collect the cycle’s overage from the renewal webhook
Each subscription.renewed webhook reports the old term’s overage by billable metric. All three API renew paths use the same shape. They are the postpaid job, prepaid POST /renew, and planned downgrades:
{
"event_id": "019d8a20-4ff5-7be0-81da-e1454b3d6f64",
"event_type": "subscription.renewed",
"tenant_id": "019d6258-07ba-7418-83be-58f5fde53e4e",
"environment": "live",
"customer_id": "019d6258-07ba-7418-83be-58f5fde53e4e",
"external_customer_id": "user_abc",
"created_at": "2026-05-01T00:00:00Z",
"idempotency_key": "sub-renew:019d...:cycle-2",
"data": {
"subscription_id": "019d8a20-4ff5-7be0-81da-e1454b3d6f70",
"customer_id": "019d6258-07ba-7418-83be-58f5fde53e4e",
"billing_mode": "postpaid",
"prior_period": { "start": "2026-04-01T00:00:00Z", "end": "2026-05-01T00:00:00Z" },
"new_period": { "start": "2026-05-01T00:00:00Z", "end": "2026-06-01T00:00:00Z" },
"usage_summary": {
"total_credits_consumed": 42000,
"net_balance_at_cycle_start": 50000,
"net_balance_at_cycle_end": 58000,
"by_billable_metric": { "image_generation": 12000, "chat_message": 30000 },
"total_overage": 2000,
"overage_by_billable_metric": { "image_generation": 2000 }
}
}
}
The payload has total_overage and overage_by_billable_metric only when the overage read works. It omits usage_summary if the old usage read fails. Handle both cases. See subscription.renewed for all fields.
Reconciling without the webhook
You do not have to depend on receiving — or replaying — the renewal webhook. GET /v1/customers/{customer_id}/overage returns per-metric overage totals for any window:
curl "https://api.quotastack.io/v1/customers/{customer_id}/overage?from=2026-04-01T00:00:00Z&to=2026-05-01T00:00:00Z" \
-H "X-API-Key: qs_live_..."
{
"data": [
{ "billable_metric_key": "image_generation", "overage_mc": 2000 }
],
"total_overage_mc": 2000,
"period": {
"from": "2026-04-01T00:00:00Z",
"to": "2026-05-01T00:00:00Z"
}
}
from and to are both required and define a half-open window [from, to) in RFC3339 — an event exactly at to belongs to the next window, so consecutive periods never double-count. data is sorted by metric key; amounts are millicredits.
| Response | When |
|---|---|
200 | Valid window. data is empty and total_overage_mc is 0 if nothing overran. |
422 | Missing from/to, unparseable RFC3339, or to not after from. |
404 | Unknown customer. Reads never create a customer. |
A by-external-id twin exists at GET /v1/customer-by-external-id/{external_id}/overage with identical parameters and response shape — use it when you only have your own identifier.
This is the path to use for a monthly reconciliation job, for re-billing a cycle whose webhook you lost, or for showing a customer their current overage mid-cycle.
Step 4: Invoice through your own PSP
QuotaStack is payment-agnostic — it records the overage but never charges anyone. Convert the millicredit overage to fiat with your own pricing and raise the invoice with your processor:
def on_subscription_renewed(event):
data = event["data"]
# usage_summary is omitted entirely if the prior-period usage read failed.
# Fall back to the overage endpoint rather than silently under-billing.
summary = data.get("usage_summary")
if summary is None:
summary = fetch_overage(data["customer_id"], data["prior_period"])
# total_overage is present only when the overage read succeeded.
total_overage = summary.get("total_overage", 0)
if total_overage > 0:
# 1 credit = 1000 millicredits. Apply your own fiat rate.
cents = round(total_overage / 1000 * PRICE_PER_CREDIT_CENTS)
stripe.Invoice.create(
# external_customer_id is on the ENVELOPE, not inside data.
customer=event["external_customer_id"],
amount=cents,
description=f"Overage: {summary.get('overage_by_billable_metric', {})}",
)
def fetch_overage(customer_id, prior_period):
"""Authoritative fallback — works for any window, webhook or not."""
r = requests.get(
f"https://api.quotastack.io/v1/customers/{customer_id}/overage",
params={"from": prior_period["start"], "to": prior_period["end"]},
headers={"X-API-Key": QS_API_KEY},
)
r.raise_for_status()
body = r.json()
return {
"total_overage": body["total_overage_mc"],
"overage_by_billable_metric": {
m["billable_metric_key"]: m["overage_mc"] for m in body["data"]
},
}
This works for prepaid and postpaid alike — the renewal payload is now the same on both, so there is no billing-mode branch to write.
The new cycle’s credit grant fires automatically, so the customer keeps working without interruption.
Gotchas
- Overage surfaces in three places. The
overagefield on the consume response,total_overage/overage_by_billable_metricon thesubscription.renewedwebhook, andGET /v1/customers/{customer_id}/overagefor any window. The last one means a lost webhook is recoverable — you are no longer forced to persist overage the moment you see it. - The balance never goes negative. Under
allow/notifythe spendable balance floors at zero and overage is tracked separately. Don’t write settlement logic that “zeroes out a negative balance” — there is nothing negative to settle; the overage figure is purely what you invoice. overage_recordedis a success.allowedistrue;debitedreflects whether any credit was drained. Don’t treat it as a denial.- Manual grants and adjustments stay strict. Overage policy applies only to consumption (consume and usage events). A negative
adjustthat exceeds the balance still returns409 Conflict, even underallow. blockaccounts don’t accrue overage. A consume that would overdraft ablockcustomer is denied (allowed: false) and nothing is recorded. Switch a customer toblockwhen their invoice goes overdue to cut off further usage.
See also
- Overage policy — the tenant default, and how one customer can override it.
- Webhooks — the
subscription.renewedpayload and delivery guarantees. - API Platform and SaaS Subscription — end-to-end postpaid walkthroughs.
Loading…