quotastack Docs
Docs / API / Usage

Usage

Record what a customer used.

A usage event says a customer used one billable metric. 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, 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 itPOST /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 oncePOST /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 awayconsumeEntitlement
    See 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-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesQueues one event for background billing. QuotaStack looks up the price and debits the ledger later, not now. May also make a new customer, the same as consumeEntitlement.
Fires credit.consumed, credit.low_balance, credit.exhausted

A usage event tells QuotaStack a customer used one billable metric, 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
FieldTypeMeaning
Idempotency-Keyrequiredstring

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
FieldTypeMeaning
customer_idoptionalstring

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_idoptionalstring

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_keyrequiredstring

Which billable metric this one event used, by key.

unitsrequiredinteger (int64)0–∞

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.

idempotency_keyrequiredstring

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_atoptionalstring (date-time)

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.

metadataoptionalobject

Your own key-value tags. QuotaStack passes them onto the ledger entry this event later writes.

If you leave it out: QuotaStack writes no tags onto the ledger entry.

Request
{
  "external_customer_id": "user_42",
  "billable_metric_key": "chat_message",
  "units": 3,
  "idempotency_key": "chat-session-482-msg-17"
}
Response 202
{
  "event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
  "idempotency_key": "chat-session-482-msg-17",
  "status": "accepted",
  "estimated_cost": 0,
  "duplicate": false
}
Response fields
FieldTypeMeaning
event_idrequiredstring (uuid)

QuotaStack’s own id for this event. Empty for a rejected one — QuotaStack never makes one.

idempotency_keyrequiredstring

Echoes the idempotency_key you sent for this event.

statusrequiredstring

Says whether QuotaStack put this event in line, or said no to it.

  • acceptedQuotaStack put this event in line, to bill for it later.
  • rejectedQuotaStack said no to this event. Nothing got queued at all.
estimated_costrequiredinteger (int64)

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 is debited. Read the ledger, or the entitlements snapshot, for the real cost.

Unit: millicredits, where 1 credit is 1000 millicredits.

duplicaterequiredboolean

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.

indexoptionalinteger

This event’s spot in the list you sent, counting from 0. Left out at spot 0. Only meaningful in a recordUsageBatch reply.

erroroptionalstring

Why QuotaStack said no to this event. Left out, not null, when status is accepted.

error_codeoptionalstring

One stable code for a rejected event. Every other case leaves this field out — read error instead.

Errors

  • 400customer_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. bad-request
  • 401Missing or invalid authentication.
  • 422billable_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. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Record a batch of usage events

POST/v1/usage/batch
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesQueues 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, credit.low_balance, 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
FieldTypeMeaning
Idempotency-Keyrequiredstring

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
FieldTypeMeaning
eventsrequiredarray

One event per entry. QuotaStack accepts up to 100 per call — 500 is what the schema states, not the real limit.

events.customer_idoptionalstring

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_idoptionalstring

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_keyrequiredstring

Which billable metric this one event used, by key.

events.unitsrequiredinteger (int64)0–∞

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.

events.idempotency_keyrequiredstring

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_atoptionalstring (date-time)

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.metadataoptionalobject

Your own key-value tags. QuotaStack passes them onto the ledger entry this event later writes.

If you leave it out: QuotaStack writes no tags onto the ledger entry.

Request
{
  "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
{
  "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
FieldTypeMeaning
acceptedrequiredinteger

How many events QuotaStack put in line here.

rejectedrequiredinteger

How many events QuotaStack turned away. Each one’s results[] entry says why.

resultsrequiredarray

One row per event you sent, in that same order. A rejected event still gets its own row here.

results.event_idrequiredstring (uuid)

QuotaStack’s own id for this event. Empty for a rejected one — QuotaStack never makes one.

results.idempotency_keyrequiredstring

Echoes the idempotency_key you sent for this event.

results.statusrequiredstring

Says whether QuotaStack put this event in line, or said no to it.

  • acceptedQuotaStack put this event in line, to bill for it later.
  • rejectedQuotaStack said no to this event. Nothing got queued at all.
results.estimated_costrequiredinteger (int64)

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 is debited. Read the ledger, or the entitlements snapshot, for the real cost.

Unit: millicredits, where 1 credit is 1000 millicredits.

results.duplicaterequiredboolean

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.indexoptionalinteger

This event’s spot in the list you sent, counting from 0. Left out at spot 0. Only meaningful in a recordUsageBatch reply.

results.erroroptionalstring

Why QuotaStack said no to this event. Left out, not null, when status is accepted.

results.error_codeoptionalstring

One stable code for a rejected event. Every other case leaves this field out — read error instead.

Errors

  • 400Send 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[]. bad-request
  • 401Missing or invalid authentication.
  • 429Rate limit exceeded.
  • 500Unexpected server error.