---
title: "Usage"
description: "What a customer used, reported after the fact — the event that debits credits and updates every total."
---

# Usage

Record what a customer used.

A usage event says a customer used one
[billable metric](/docs/concepts/metering). The event also says how many
units. You send it once the work is done.

QuotaStack puts the event in line first. QuotaStack prices it later, in the
background. QuotaStack finds the price, debits the
[ledger](/docs/concepts/credits), and updates every total that depends on it.
All of this happens after your call already got its reply.

## Which call do I want?

- The action already happened, and you only need to bill for it → `POST /v1/usage`
  QuotaStack queues the event and bills it in the background. This call never blocks the action itself.
- You have many events to bill at once → `POST /v1/usage/batch`
  One bad event does not fail the rest. Read the per-event `results[]` array to see which ones landed.
- You want to gate the action AND charge for it, in one call, with the result right away → `consumeEntitlement`
  See [Entitlements](/docs/api/entitlements). `recordUsage` never blocks anything — it only bills, after the fact, once you have already decided to let the action through.

2 of 2 operations documented in full.

## Record a usage event

`POST /v1/usage`

- **Idempotency-Key:** Required. Returns 422 without it.
- **Environment:** Sandbox and Live
- **Auth:** Tenant API key
- **Writes:** Queues one event for background billing. QuotaStack looks up the price and debits the [ledger](/docs/concepts/credits) later, not now. May also make a new customer, the same as `consumeEntitlement`.
- **Fires:** [`credit.consumed`](/docs/api/events/credit.consumed), [`credit.low_balance`](/docs/api/events/credit.low_balance), [`credit.exhausted`](/docs/api/events/credit.exhausted)

A usage event tells QuotaStack a customer used one [billable metric](/docs/concepts/metering), after the fact. QuotaStack queues the event, and bills it later, in the background. This call does not wait for that debit. Its response never carries the real cost. An unrecognized customer id is not an error here. QuotaStack makes a new customer for it, the same as a credit grant. Two failure modes here have no visible error at all. A bad metric key retries a few times in the background. Then it lands in a dead-letter queue an operator can inspect. Not enough credit, under a block policy, is worse. QuotaStack drops the event right away. No retry. No dead-letter entry. Nothing bills for that event. Nothing tells you it happened, either.

### Header parameters

| Field | Type | Required | Meaning |
|---|---|---|---|
| `Idempotency-Key` | string | required | A unique string you choose, up to 256 characters. It stops a retry from doing the work twice. This header is required on every POST and PATCH. Leave it out and the request fails with `422`, before any change is made. Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries `X-Idempotent-Replayed: true`. Replays are available for 24 hours. Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example `topup:{payment_intent_id}` or `renewal:{subscription_id}:{period_start}`. |

### Body parameters

| Field | Type | Required | Meaning |
|---|---|---|---|
| `customer_id` | string | optional | QuotaStack’s own id for the customer, kept here for old code that still sends it this way. If you leave it out: Fine to leave out if you send `external_customer_id` instead. Leaving out both is rejected. |
| `external_customer_id` | string | optional | Your own id for this customer. Use this one — it never falls back to a UUID lookup. If you leave it out: Fine to leave out if you send `customer_id` instead. Leaving out both is rejected. |
| `billable_metric_key` | string | required | Which [billable metric](/docs/concepts/metering) this one event used, by key. |
| `units` | integer (int64) | required | How many units this event used. The schema allows 0, but QuotaStack rejects 0 or less. Unit: a count of billable metric units, not credits. Range: 0–∞. |
| `idempotency_key` | string | required | Your own key for this one event, not the same as the `Idempotency-Key` header. Send it again within a day and QuotaStack will not bill it twice. The reply shows `duplicate: true`, plus the first event’s own `event_id`. QuotaStack never checks the rest of the event here. The same key still replays, even with new `units` or a new customer. |
| `occurred_at` | string (date-time) | optional | When the customer did this, if not right now. Must not be more than a minute in the future. If you leave it out: QuotaStack stores the time it received the call instead. |
| `metadata` | object | optional | Your own key-value tags. QuotaStack passes them onto the [ledger](/docs/concepts/credits) entry this event later writes. If you leave it out: QuotaStack writes no tags onto the [ledger](/docs/concepts/credits) entry. |

### Request

```json
{
  "external_customer_id": "user_42",
  "billable_metric_key": "chat_message",
  "units": 3,
  "idempotency_key": "chat-session-482-msg-17"
}
```

### Response 202

```json
{
  "event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
  "idempotency_key": "chat-session-482-msg-17",
  "status": "accepted",
  "estimated_cost": 0,
  "duplicate": false
}
```

### Response fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `event_id` | string (uuid) | required | QuotaStack’s own id for this event. Empty for a rejected one — QuotaStack never makes one. |
| `idempotency_key` | string | required | Echoes the `idempotency_key` you sent for this event. |
| `status` | string | required | Says whether QuotaStack put this event in line, or said no to it. `accepted` — QuotaStack put this event in line, to bill for it later. `rejected` — QuotaStack said no to this event. Nothing got queued at all. |
| `estimated_cost` | integer (int64) | required | Always `0` today. QuotaStack has not priced this event yet. Recording usage is asynchronous. The real cost comes later, once the queued event is processed and the [ledger](/docs/concepts/credits) is debited. Read the ledger, or the [entitlements](/docs/concepts/entitlements) snapshot, for the real cost. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `duplicate` | boolean | required | True if you sent this same key in the last day. QuotaStack did not queue this event twice; `event_id` is the first one’s. |
| `index` | integer | optional | This event’s spot in the list you sent, counting from 0. Left out at spot 0. Only meaningful in a `recordUsageBatch` reply. |
| `error` | string | optional | Why QuotaStack said no to this event. Left out, not `null`, when `status` is `accepted`. |
| `error_code` | string | optional | One stable code for a rejected event. Every other case leaves this field out — read `error` instead. |

### Errors

- `400` — `customer_id` and `external_customer_id` are both empty, or both set — send exactly one. A `customer_id` from another tenant returns this too. So does a body QuotaStack cannot parse as JSON. See [bad-request](/docs/api/errors/bad-request).
- `401` — Missing or invalid authentication.
- `422` — `billable_metric_key` or `idempotency_key` is missing, `units` is zero or less, or `occurred_at` is more than a minute in the future. The response names the field. See [validation-error](/docs/api/errors/validation-error).
- `429` — Rate limit exceeded.
- `500` — Unexpected server error.

### See also

- [Metering](/docs/concepts/metering)
- [Credits](/docs/concepts/credits)
- [Idempotency](/docs/concepts/idempotency)
- [Entitlements](/docs/api/entitlements)

## Record a batch of usage events

`POST /v1/usage/batch`

- **Idempotency-Key:** Required. Returns 422 without it.
- **Environment:** Sandbox and Live
- **Auth:** Tenant API key
- **Writes:** Queues every good event, one at a time, the same way `recordUsage` does. A bad event is skipped alone. The rest still go through.
- **Fires:** [`credit.consumed`](/docs/api/events/credit.consumed), [`credit.low_balance`](/docs/api/events/credit.low_balance), [`credit.exhausted`](/docs/api/events/credit.exhausted)

This queues many usage events in one call, up to 100 of them. openapi.yaml’s own schema states 500, but 100 is the real cap. A bigger batch returns `400` for the whole call. QuotaStack’s build sends back `202`, not the `200` openapi.yaml states. One bad event never fails the rest. QuotaStack rejects that one event alone, and still queues every good one. Check `accepted` and `rejected` in the body to see what happened. The same two silent failures apply here, per event. A bad metric key retries, then lands in a dead-letter queue. Not enough credit, under a block policy, vanishes instead. No trace anywhere. See `recordUsage`’s own intro for both.

### Header parameters

| Field | Type | Required | Meaning |
|---|---|---|---|
| `Idempotency-Key` | string | required | A unique string you choose, up to 256 characters. It stops a retry from doing the work twice. This header is required on every POST and PATCH. Leave it out and the request fails with `422`, before any change is made. Send the same key twice and QuotaStack replays the first response. The second call's status and body match the first, and the response carries `X-Idempotent-Replayed: true`. Replays are available for 24 hours. Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example `topup:{payment_intent_id}` or `renewal:{subscription_id}:{period_start}`. |

### Body parameters

| Field | Type | Required | Meaning |
|---|---|---|---|
| `events` | array | required | One event per entry. QuotaStack accepts up to 100 per call — 500 is what the schema states, not the real limit. |
| `events.customer_id` | string | optional | QuotaStack’s own id for the customer, kept here for old code that still sends it this way. If you leave it out: Fine to leave out if you send `external_customer_id` instead. Leaving out both is rejected. |
| `events.external_customer_id` | string | optional | Your own id for this customer. Use this one — it never falls back to a UUID lookup. If you leave it out: Fine to leave out if you send `customer_id` instead. Leaving out both is rejected. |
| `events.billable_metric_key` | string | required | Which [billable metric](/docs/concepts/metering) this one event used, by key. |
| `events.units` | integer (int64) | required | How many units this event used. The schema allows 0, but QuotaStack rejects 0 or less. Unit: a count of billable metric units, not credits. Range: 0–∞. |
| `events.idempotency_key` | string | required | Your own key for this one event, not the same as the `Idempotency-Key` header. Send it again within a day and QuotaStack will not bill it twice. The reply shows `duplicate: true`, plus the first event’s own `event_id`. QuotaStack never checks the rest of the event here. The same key still replays, even with new `units` or a new customer. |
| `events.occurred_at` | string (date-time) | optional | When the customer did this, if not right now. Must not be more than a minute in the future. If you leave it out: QuotaStack stores the time it received the call instead. |
| `events.metadata` | object | optional | Your own key-value tags. QuotaStack passes them onto the [ledger](/docs/concepts/credits) entry this event later writes. If you leave it out: QuotaStack writes no tags onto the [ledger](/docs/concepts/credits) entry. |

### Request

```json
{
  "events": [
    {
      "external_customer_id": "user_42",
      "billable_metric_key": "chat_message",
      "units": 3,
      "idempotency_key": "chat-session-482-msg-17"
    },
    {
      "external_customer_id": "user_42",
      "billable_metric_key": "chat_message",
      "units": 0,
      "idempotency_key": "chat-session-482-msg-18"
    }
  ]
}
```

### Response 200

```json
{
  "accepted": 1,
  "rejected": 1,
  "results": [
    {
      "event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
      "idempotency_key": "chat-session-482-msg-17",
      "status": "accepted",
      "estimated_cost": 0,
      "duplicate": false
    },
    {
      "event_id": "",
      "idempotency_key": "chat-session-482-msg-18",
      "status": "rejected",
      "estimated_cost": 0,
      "duplicate": false,
      "index": 1,
      "error": "units must be positive"
    }
  ]
}
```

### Response fields

| Field | Type | Required | Meaning |
|---|---|---|---|
| `accepted` | integer | required | How many events QuotaStack put in line here. |
| `rejected` | integer | required | How many events QuotaStack turned away. Each one’s `results[]` entry says why. |
| `results` | array | required | One row per event you sent, in that same order. A rejected event still gets its own row here. |
| `results.event_id` | string (uuid) | required | QuotaStack’s own id for this event. Empty for a rejected one — QuotaStack never makes one. |
| `results.idempotency_key` | string | required | Echoes the `idempotency_key` you sent for this event. |
| `results.status` | string | required | Says whether QuotaStack put this event in line, or said no to it. `accepted` — QuotaStack put this event in line, to bill for it later. `rejected` — QuotaStack said no to this event. Nothing got queued at all. |
| `results.estimated_cost` | integer (int64) | required | Always `0` today. QuotaStack has not priced this event yet. Recording usage is asynchronous. The real cost comes later, once the queued event is processed and the [ledger](/docs/concepts/credits) is debited. Read the ledger, or the [entitlements](/docs/concepts/entitlements) snapshot, for the real cost. Unit: millicredits, where 1 credit is 1000 millicredits. |
| `results.duplicate` | boolean | required | True if you sent this same key in the last day. QuotaStack did not queue this event twice; `event_id` is the first one’s. |
| `results.index` | integer | optional | This event’s spot in the list you sent, counting from 0. Left out at spot 0. Only meaningful in a `recordUsageBatch` reply. |
| `results.error` | string | optional | Why QuotaStack said no to this event. Left out, not `null`, when `status` is `accepted`. |
| `results.error_code` | string | optional | One stable code for a rejected event. Every other case leaves this field out — read `error` instead. |

### Errors

- `400` — Send at least one event, and no more than 100. A bigger list gets this error. A body QuotaStack cannot read as JSON gets this too. Every other bad event fails alone, inside `results[]`. See [bad-request](/docs/api/errors/bad-request).
- `401` — Missing or invalid authentication.
- `429` — Rate limit exceeded.
- `500` — Unexpected server error.

### See also

- [Metering](/docs/concepts/metering)
- [Credits](/docs/concepts/credits)
- [Idempotency](/docs/concepts/idempotency)
- [Entitlements](/docs/api/entitlements)
