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 it →
POST /v1/usageQuotaStack 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/batchOne bad event does not fail the rest. Read the per-eventresults[]array to see which ones landed. - You want to gate the action AND charge for it, in one call, with the result right away →
consumeEntitlementSee Entitlements.recordUsagenever blocks anything — it only bills, after the fact, once you have already decided to let the action through.
Record a usage event
/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 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.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
customer_idoptional | string | 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_idoptional | string | 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 |
billable_metric_keyrequired | string | Which billable metric this one event used, by key. |
unitsrequired | integer (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_keyrequired | string | Your own key for this one event, not the same as the |
occurred_atoptional | string (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. |
metadataoptional | object | 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. |
{
"external_customer_id": "user_42",
"billable_metric_key": "chat_message",
"units": 3,
"idempotency_key": "chat-session-482-msg-17"
} {
"event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
"idempotency_key": "chat-session-482-msg-17",
"status": "accepted",
"estimated_cost": 0,
"duplicate": false
} | Field | Type | Meaning |
|---|---|---|
event_idrequired | string (uuid) | QuotaStack’s own id for this event. Empty for a rejected one — QuotaStack never makes one. |
idempotency_keyrequired | string | Echoes the |
statusrequired | string | Says whether QuotaStack put this event in line, or said no to it.
|
estimated_costrequired | integer (int64) | Always Unit: millicredits, where 1 credit is 1000 millicredits. |
duplicaterequired | boolean | True if you sent this same key in the last day. QuotaStack did not queue this event twice; |
indexoptional | integer | This event’s spot in the list you sent, counting from 0. Left out at spot 0. Only meaningful in a |
erroroptional | string | Why QuotaStack said no to this event. Left out, not |
error_codeoptional | string | One stable code for a rejected event. Every other case leaves this field out — read |
Errors
400—customer_idandexternal_customer_idare both empty, or both set — send exactly one. Acustomer_idfrom another tenant returns this too. So does a body QuotaStack cannot parse as JSON. bad-request401— Missing or invalid authentication.422—billable_metric_keyoridempotency_keyis missing,unitsis zero or less, oroccurred_atis more than a minute in the future. The response names the field. validation-error429— Rate limit exceeded.500— Unexpected server error.
Record a batch of usage events
/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, 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.
| Field | Type | Meaning |
|---|---|---|
Idempotency-Keyrequired | string | 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 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 Derive the key from the business event rather than a random value, so a retry from anywhere reuses it — for example |
| Field | Type | Meaning |
|---|---|---|
eventsrequired | array | One event per entry. QuotaStack accepts up to 100 per call — 500 is what the schema states, not the real limit. |
events.customer_idoptional | string | 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 |
events.external_customer_idoptional | string | 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 |
events.billable_metric_keyrequired | string | Which billable metric this one event used, by key. |
events.unitsrequired | integer (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_keyrequired | string | Your own key for this one event, not the same as the |
events.occurred_atoptional | string (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.metadataoptional | object | 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. |
{
"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"
}
]
} {
"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"
}
]
} | Field | Type | Meaning |
|---|---|---|
acceptedrequired | integer | How many events QuotaStack put in line here. |
rejectedrequired | integer | How many events QuotaStack turned away. Each one’s |
resultsrequired | array | One row per event you sent, in that same order. A rejected event still gets its own row here. |
results.event_idrequired | string (uuid) | QuotaStack’s own id for this event. Empty for a rejected one — QuotaStack never makes one. |
results.idempotency_keyrequired | string | Echoes the |
results.statusrequired | string | Says whether QuotaStack put this event in line, or said no to it.
|
results.estimated_costrequired | integer (int64) | Always Unit: millicredits, where 1 credit is 1000 millicredits. |
results.duplicaterequired | boolean | True if you sent this same key in the last day. QuotaStack did not queue this event twice; |
results.indexoptional | integer | This event’s spot in the list you sent, counting from 0. Left out at spot 0. Only meaningful in a |
results.erroroptional | string | Why QuotaStack said no to this event. Left out, not |
results.error_codeoptional | string | One stable code for a rejected event. Every other case leaves this field out — read |
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, insideresults[]. bad-request401— Missing or invalid authentication.429— Rate limit exceeded.500— Unexpected server error.
Loading…