---
title: "Billing Overage in Arrears"
description: "Let customers keep working past their credit balance, then invoice the recorded overage each cycle through your own PSP. QuotaStack stays payment-agnostic."
order: 7
---

# 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](/docs/concepts/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:

1. Drains whatever spendable credit exists, flooring the balance at **zero**.
2. Records the uncovered remainder as an append-only **overage event**.
3. Surfaces that amount inline on the consume response and rolls it into the next postpaid `subscription.renewed` webhook.

`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:

```bash
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](/docs/concepts/entitlements#configuring-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:

```bash
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:

```json
{
  "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: true` and `reason: "overage_recorded"` mean the request went through. This is **informational, not an error**.
- `overage: 2000` is the millicredits recorded for arrears billing — present only when overage accrued.
- `balance` stays at `0`. 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:

```json
{
  "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`](/docs/api/events/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:

```bash
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_..."
```

```json
{
  "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:

```python
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 `overage` field on the consume response, `total_overage` / `overage_by_billable_metric` on the `subscription.renewed` webhook, and `GET /v1/customers/{customer_id}/overage` for 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`/`notify` the 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_recorded` is a success.** `allowed` is `true`; `debited` reflects 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 `adjust` that exceeds the balance still returns `409 Conflict`, even under `allow`.
- **`block` accounts don't accrue overage.** A consume that would overdraft a `block` customer is denied (`allowed: false`) and nothing is recorded. Switch a customer to `block` when their invoice goes overdue to cut off further usage.

## See also

- [Overage policy](/docs/concepts/entitlements#configuring-overage-policy) — the tenant default, and how one customer can override it.
- [Webhooks](/docs/concepts/webhooks) — the `subscription.renewed` payload and delivery guarantees.
- [API Platform](/docs/use-cases/api-platform) and [SaaS Subscription](/docs/use-cases/saas-subscription) — end-to-end postpaid walkthroughs.
