quotastack Docs
Docs / API / Reservations

Reservations

Hold credits while a long job runs.

A reservation holds credits before you know the real cost. You reserve an estimate, run the job, then settle the true number afterward.

Long AI jobs are why this pattern exists. Nobody knows how many tokens a job will burn until it finishes. A plain charge cannot wait that long.

Reserving locks estimated_cost inside reserved_balance. balance stays untouched — the hold only blocks those same credits from being spent twice. effective_balance drops by the same amount, since a new charge can never spend a held credit.

Two ways to close a reservation. Commit it once you know the real cost. QuotaStack charges actual_units, then returns whatever is left over. Release it when the job fails, or costs nothing. QuotaStack returns the whole hold and charges nothing.

A hold is not a charge. Commit charges the balance; release gives the hold back.

Nobody has to close a reservation by hand. Every reservation carries an expires_at. QuotaStack’s scheduler sweeps for expired holds once a minute, and releases them the same way a manual release would.

Already know the cost? Skip the hold. Call consumeEntitlement instead.

Which call do I want?

6 of 6 operations documented in full.

Reserve credits

POST/v1/reservations
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne new reservation, and one zero-amount ledger entry, type reservation, for the audit trail. Raises reserved_balance by estimated_cost; balance is unchanged.
Fires credit.low_balance, credit.exhausted

This call holds credits before you know the exact cost of a job. Send customer_id or external_customer_id, never both. QuotaStack rejects the call with neither. An unrecognized value on either field creates a new customer, the same lookup grantCustomerCredits uses. QuotaStack prices estimated_units against the metric's active metering rule. QuotaStack holds that price in reserved_balance. balance itself stays the same. Leave out ttl_seconds and the hold stays open for 1800 seconds, 30 minutes. Send your own value, from 1 second up to 86400, 24 hours. A hold lowers effective_balance, the same as a real charge. So this call can trigger the low-balance and exhausted webhooks too, best-effort. This reply is not the full Reservation object the field table below describes. The real reply adds a nested account balance and drops actual_units, actual_cost, reference_id, committed_at, and released_at. openapi.yaml declares 404 and 402 here. Neither one is real. An unrecognized customer ID never returns 404 — QuotaStack makes the customer instead. A billable_metric_key with no active metering rule returns 500, not 404. A real insufficient-credit failure returns 409, type insufficient-credits, never 402.

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 this customer, or your own lenient ID. Send this or external_customer_id, never both.

If you leave it out: Send external_customer_id instead. QuotaStack rejects the call with neither.

external_customer_idoptionalstring

Your own ID for this customer. Looked up, or created, strictly by this value — no UUID fallback.

If you leave it out: Send customer_id instead. QuotaStack rejects the call with neither.

billable_metric_keyrequiredstring

Which billable metric to price the hold against. Needs an active metering rule.

If you leave it out: QuotaStack rejects the call. This field is required.

estimated_unitsrequiredinteger (int64)1–∞

How many units you expect the job to use.

Unit: a count of billable metric units, not credits.

If you leave it out: QuotaStack rejects the call. Must be at least 1.

ttl_secondsoptionalinteger1–86400

How long the hold stays open before it lapses on its own.

Unit: seconds.

If you leave it out: The hold stays open for 1800 seconds — 30 minutes — by default.

metadataoptionalobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: An empty object is stored instead.

Request
{
  "external_customer_id": "user_42",
  "billable_metric_key": "chat_message",
  "estimated_units": 500,
  "ttl_seconds": 1800
}
Response 201
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "billable_metric_key": "chat_message",
  "estimated_units": 500,
  "estimated_cost": 250000,
  "environment": "live",
  "status": "active",
  "expires_at": "2026-07-27T10:30:00Z",
  "account": {
    "balance": 500000,
    "reserved_balance": 250000,
    "effective_balance": 250000
  },
  "metadata": {},
  "created_at": "2026-07-27T10:00:00Z"
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

billable_metric_keyrequiredstring

Which billable metric this hold is priced against.

estimated_unitsrequiredinteger (int64)

How many units you expect the job to use. QuotaStack prices the hold from this number.

Unit: a count of billable metric units, not credits.

estimated_costrequiredinteger (int64)

What the hold costs, at today's price for estimated_units. QuotaStack subtracts this from effective_balance while the reservation stays active.

Unit: millicredits, where 1 credit is 1000 millicredits.

actual_unitsoptionalinteger (int64)

How many units the job really used. null until you commit the reservation.

Unit: a count of billable metric units, not credits.

actual_costoptionalinteger (int64)

What QuotaStack charged for actual_units. Priced when you commit, which can differ from the rate used to estimate the hold. null until you commit it. Can read higher than what QuotaStack actually collected, if the charge outran both the hold and your free balance.

Unit: millicredits.

statusrequiredstring

The hold's current state, right now.

  • activeThe hold is open. Commit it, release it, or wait for it to expire.
  • committedYou committed it. QuotaStack charged actual_units and returned the rest of the hold.
  • releasedYou released it before it was committed. QuotaStack returned the whole hold.
  • expiredNobody closed it in time. QuotaStack's scheduler released the hold on its own.
reference_idrequiredstring (uuid)

QuotaStack's own ID for this hold's ledger entries. The hold, the commit, and any release all share it.

expires_atrequiredstring (date-time)

When the hold lapses on its own, if nobody commits or releases it first. Set once, from ttl_seconds, at creation.

metadatarequiredobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: An empty object is stored instead.

created_atrequiredstring (date-time)
committed_atoptionalstring (date-time)

When you committed it. null unless status is committed.

released_atoptionalstring (date-time)

When the hold was given back. null unless status is released or expired — committing with actual_units: 0 sets committed_at instead, not this field.

Errors

  • 400Sending both customer_id and external_customer_id, or neither, returns this. So does a customer_id UUID that belongs to a different tenant. bad-request
  • 402openapi.yaml lists this status here. QuotaStack never sends it. A real failure returns 409 instead, type insufficient-credits. This call's own spec entry never lists 409 at all. insufficient-credits
  • 404openapi.yaml lists this status. Nothing here returns it. An unrecognized customer_id makes a new customer instead. So does an unrecognized external_customer_id. A billable_metric_key needs a priced metering rule. Without one, this call returns 500 instead. not-found

Retrieve a reservation

GET/v1/reservations/{id}
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This call returns one reservation by its own id. Check whether a hold is still active before committing or releasing it, or use this to debug one that seems stuck. This lookup is not scoped by environment. A sandbox key can read a hold created under a live key for the same tenant, and the reverse. That gap also applies to commitReservation and releaseReservation, both of which fetch the same way.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)
Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "environment": "live",
  "billable_metric_key": "chat_message",
  "estimated_units": 500,
  "estimated_cost": 250000,
  "actual_units": null,
  "actual_cost": null,
  "status": "active",
  "expires_at": "2026-07-27T10:30:00Z",
  "metadata": {},
  "created_at": "2026-07-27T10:00:00Z",
  "committed_at": null,
  "released_at": null
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

billable_metric_keyrequiredstring

Which billable metric this hold is priced against.

estimated_unitsrequiredinteger (int64)

How many units you expect the job to use. QuotaStack prices the hold from this number.

Unit: a count of billable metric units, not credits.

estimated_costrequiredinteger (int64)

What the hold costs, at today's price for estimated_units. QuotaStack subtracts this from effective_balance while the reservation stays active.

Unit: millicredits, where 1 credit is 1000 millicredits.

actual_unitsoptionalinteger (int64)

How many units the job really used. null until you commit the reservation.

Unit: a count of billable metric units, not credits.

actual_costoptionalinteger (int64)

What QuotaStack charged for actual_units. Priced when you commit, which can differ from the rate used to estimate the hold. null until you commit it. Can read higher than what QuotaStack actually collected, if the charge outran both the hold and your free balance.

Unit: millicredits.

statusrequiredstring

The hold's current state, right now.

  • activeThe hold is open. Commit it, release it, or wait for it to expire.
  • committedYou committed it. QuotaStack charged actual_units and returned the rest of the hold.
  • releasedYou released it before it was committed. QuotaStack returned the whole hold.
  • expiredNobody closed it in time. QuotaStack's scheduler released the hold on its own.
reference_idrequiredstring (uuid)

QuotaStack's own ID for this hold's ledger entries. The hold, the commit, and any release all share it.

expires_atrequiredstring (date-time)

When the hold lapses on its own, if nobody commits or releases it first. Set once, from ttl_seconds, at creation.

metadatarequiredobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: An empty object is stored instead.

created_atrequiredstring (date-time)
committed_atoptionalstring (date-time)

When you committed it. null unless status is committed.

released_atoptionalstring (date-time)

When the hold was given back. null unless status is released or expired — committing with actual_units: 0 sets committed_at instead, not this field.

Errors

See also

Commit a reservation

POST/v1/reservations/{id}/commit
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesDebits actual_cost from the balance and releases the whole hold, in one step. Writes one ledger entry, type consumption, for the debit. Fires credit.consumed, plus the low-balance and exhausted alerts — the same as a usage debit. actual_units: 0 skips all of that. No debit happens, and credit.consumed never fires. QuotaStack writes a zero-amount release entry instead.
Fires credit.consumed, credit.low_balance, credit.exhausted

Commit closes an active reservation once you know the real cost. QuotaStack prices actual_units at today's rate. That rate can differ from the one used to estimate the hold. QuotaStack charges that amount, then returns the rest. Using less than you reserved needs no separate release call. The leftover comes back in this same call. Using more than the hold covers, QuotaStack also tries your free balance for the difference. The call still succeeds if the hold plus your free balance covers actual_units. In that case, actual_cost in the response is still the full computed number. That can read higher than what QuotaStack actually collected. Send actual_units: 0 for a job that made nothing. QuotaStack returns the whole hold and charges nothing, the same as releaseReservation. But status reads committed here, not released. A negative actual_units returns 422, a status this call does not declare. This reply is not the full Reservation object the field table below describes. The real reply uses reservation_id, not id. The real reply drops most of the other fields too. Send a key unique to this hold, for example commit:{id} — the idempotency page's own convention. QuotaStack scopes a key by your tenant and its value alone, never by the reservation in the path. Reuse a key across two holds with the same body. The second call replays the first instead of committing it.

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

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)
Body parameters
FieldTypeMeaning
actual_unitsrequiredinteger (int64)0–∞

How many units the job really used.

Unit: a count of billable metric units, not credits.

If you leave it out: QuotaStack rejects the call. This field is required; 0 is a valid value and releases the whole hold.

metadataoptionalobject

Your own key-value tags. QuotaStack merges them into this hold's stored tags.

If you leave it out: Nothing changes. QuotaStack also discards metadata when actual_units is 0 — that path never merges it.

Request
{
  "actual_units": 420
}
Response 200
{
  "reservation_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
  "status": "committed",
  "estimated_units": 500,
  "actual_units": 420,
  "estimated_cost": 250000,
  "actual_cost": 210000,
  "released": 40000,
  "transaction": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
    "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
    "delta": -210000,
    "type": "consumption",
    "billable_metric_key": "chat_message",
    "cost_units": 420,
    "created_at": "2026-07-27T10:05:00Z"
  },
  "account": {
    "balance": 290000,
    "reserved_balance": 0,
    "effective_balance": 290000
  }
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

billable_metric_keyrequiredstring

Which billable metric this hold is priced against.

estimated_unitsrequiredinteger (int64)

How many units you expect the job to use. QuotaStack prices the hold from this number.

Unit: a count of billable metric units, not credits.

estimated_costrequiredinteger (int64)

What the hold costs, at today's price for estimated_units. QuotaStack subtracts this from effective_balance while the reservation stays active.

Unit: millicredits, where 1 credit is 1000 millicredits.

actual_unitsoptionalinteger (int64)

How many units the job really used. null until you commit the reservation.

Unit: a count of billable metric units, not credits.

actual_costoptionalinteger (int64)

What QuotaStack charged for actual_units. Priced when you commit, which can differ from the rate used to estimate the hold. null until you commit it. Can read higher than what QuotaStack actually collected, if the charge outran both the hold and your free balance.

Unit: millicredits.

statusrequiredstring

The hold's current state, right now.

  • activeThe hold is open. Commit it, release it, or wait for it to expire.
  • committedYou committed it. QuotaStack charged actual_units and returned the rest of the hold.
  • releasedYou released it before it was committed. QuotaStack returned the whole hold.
  • expiredNobody closed it in time. QuotaStack's scheduler released the hold on its own.
reference_idrequiredstring (uuid)

QuotaStack's own ID for this hold's ledger entries. The hold, the commit, and any release all share it.

expires_atrequiredstring (date-time)

When the hold lapses on its own, if nobody commits or releases it first. Set once, from ttl_seconds, at creation.

metadatarequiredobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: An empty object is stored instead.

created_atrequiredstring (date-time)
committed_atoptionalstring (date-time)

When you committed it. null unless status is committed.

released_atoptionalstring (date-time)

When the hold was given back. null unless status is released or expired — committing with actual_units: 0 sets committed_at instead, not this field.

What comes back in each case

Used less than reserved

The common case for an AI job. QuotaStack charges the real cost and returns the difference.

{
  "reservation_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
  "status": "committed",
  "estimated_units": 500,
  "actual_units": 420,
  "estimated_cost": 250000,
  "actual_cost": 210000,
  "released": 40000,
  "transaction": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
    "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
    "delta": -210000,
    "type": "consumption",
    "billable_metric_key": "chat_message",
    "cost_units": 420,
    "created_at": "2026-07-27T10:05:00Z"
  },
  "account": {
    "balance": 290000,
    "reserved_balance": 0,
    "effective_balance": 290000
  }
}
Used exactly what you reserved

Nothing comes back. released reads 0.

{
  "reservation_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
  "status": "committed",
  "estimated_units": 500,
  "actual_units": 500,
  "estimated_cost": 250000,
  "actual_cost": 250000,
  "released": 0,
  "transaction": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
    "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
    "delta": -250000,
    "type": "consumption",
    "billable_metric_key": "chat_message",
    "cost_units": 500,
    "created_at": "2026-07-27T10:05:00Z"
  },
  "account": {
    "balance": 250000,
    "reserved_balance": 0,
    "effective_balance": 250000
  }
}
The job produced nothing

actual_units: 0 returns the whole hold and charges nothing. status reads committed, not released.

{
  "reservation_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
  "status": "committed",
  "estimated_units": 500,
  "actual_units": 0,
  "estimated_cost": 250000,
  "actual_cost": 0,
  "released": 250000,
  "transaction": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
    "delta": 0,
    "type": "release",
    "reference_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
    "created_at": "2026-07-27T10:05:00Z"
  },
  "account": {
    "balance": 500000,
    "reserved_balance": 0,
    "effective_balance": 500000
  }
}

Errors

  • 402openapi.yaml lists this status here. QuotaStack never sends it. A real failure returns 409 instead. See that row below. insufficient-credits
  • 404No reservation here has this id. not-found
  • 409QuotaStack returns this for three reasons. Committing after expires_at, before the scheduler's sweep catches up, is reservation-expired. That window lasts about a minute. Once the sweep runs, status already reads expired. Committing then returns the plainer conflict cause instead — same root problem, less specific. The hold leaving active some other way is conflict too — already committed, released, or expired. Reusing an Idempotency-Key whose stored request matches this one exactly is conflict as well. QuotaStack replays the first response instead of failing. The hold plus your free balance falling short of actual_cost is insufficient-credits, the third reason. The reservation stays active in every case here. reservation-expired

Release a reservation

POST/v1/reservations/{id}/release
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesReturns the whole hold to the balance and writes one zero-amount ledger entry, type release, for the audit trail. Charges nothing and fires no webhook.
Fires No webhooks.

Release closes an active reservation without charging anything. QuotaStack returns the whole estimated_cost to the balance. This call takes no request body. Release never checks expires_at. Releasing a hold that already expired, but is not yet swept, still succeeds. That produces the same result the sweep would have. This call always sends an empty body, so every release request hashes the same. Send a key unique to this hold, for example release:{id}. Reuse a key across two reservations, and the second call always replays the first. QuotaStack never returns a conflict here, since the body never changes.

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

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)
Response 200
{
  "reservation_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
  "status": "released",
  "estimated_cost": 250000,
  "released": 250000,
  "transaction": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
    "delta": 0,
    "type": "release",
    "reference_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
    "created_at": "2026-07-27T10:10:00Z"
  },
  "account": {
    "balance": 500000,
    "reserved_balance": 0,
    "effective_balance": 500000
  }
}
Response fields
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

billable_metric_keyrequiredstring

Which billable metric this hold is priced against.

estimated_unitsrequiredinteger (int64)

How many units you expect the job to use. QuotaStack prices the hold from this number.

Unit: a count of billable metric units, not credits.

estimated_costrequiredinteger (int64)

What the hold costs, at today's price for estimated_units. QuotaStack subtracts this from effective_balance while the reservation stays active.

Unit: millicredits, where 1 credit is 1000 millicredits.

actual_unitsoptionalinteger (int64)

How many units the job really used. null until you commit the reservation.

Unit: a count of billable metric units, not credits.

actual_costoptionalinteger (int64)

What QuotaStack charged for actual_units. Priced when you commit, which can differ from the rate used to estimate the hold. null until you commit it. Can read higher than what QuotaStack actually collected, if the charge outran both the hold and your free balance.

Unit: millicredits.

statusrequiredstring

The hold's current state, right now.

  • activeThe hold is open. Commit it, release it, or wait for it to expire.
  • committedYou committed it. QuotaStack charged actual_units and returned the rest of the hold.
  • releasedYou released it before it was committed. QuotaStack returned the whole hold.
  • expiredNobody closed it in time. QuotaStack's scheduler released the hold on its own.
reference_idrequiredstring (uuid)

QuotaStack's own ID for this hold's ledger entries. The hold, the commit, and any release all share it.

expires_atrequiredstring (date-time)

When the hold lapses on its own, if nobody commits or releases it first. Set once, from ttl_seconds, at creation.

metadatarequiredobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: An empty object is stored instead.

created_atrequiredstring (date-time)
committed_atoptionalstring (date-time)

When you committed it. null unless status is committed.

released_atoptionalstring (date-time)

When the hold was given back. null unless status is released or expired — committing with actual_units: 0 sets committed_at instead, not this field.

Errors

  • 404No reservation here has this id. not-found
  • 409QuotaStack returns this for two reasons. The hold already left active, either committed, released, or expired. Or you sent an Idempotency-Key that matches an old request. QuotaStack replays the old reply instead of failing. conflict

List a customer's reservations

GET/v1/customers/{customer_id}/reservations
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This call lists every reservation for one customer, newest first. Filter by status. Leave it out for every status. This lookup is lenient. customer_id takes your own external_id or QuotaStack's own id. This lookup never makes a new customer. An unrecognized customer_id returns 404 here. openapi.yaml does not declare that; only 200 is listed. An invalid cursor returns 400, also undeclared. An unrecognized status value returns 500, also undeclared. status binds to a raw database column. QuotaStack never checks it first. This call returns the real Reservation shape. reserveCredits, commitReservation, and releaseReservation do not.

Path parameters
FieldTypeMeaning
customer_idrequiredstring

Customer identifier. Can be either a QuotaStack UUID or your own external_id — the server resolves both. For strict external-id resolution, use the /v1/customer-by-external-id/... path family instead.

Query parameters
FieldTypeMeaning
statusoptionalstring

Only holds in this state.

If you leave it out: Every status comes back, with no filter.

  • activeThe hold is open. Commit it, release it, or wait for it to expire.
  • committedYou committed it. QuotaStack charged actual_units and returned the rest of the hold.
  • releasedYou released it before it was committed. QuotaStack returned the whole hold.
  • expiredNobody closed it in time. QuotaStack's scheduler released the hold on its own.
cursoroptionalstring

Opaque pagination cursor from the previous page's pagination.next_cursor. Omit to start at the first page.

limitoptionalinteger

Page size. Default 20, max 100.

Response 200
{
  "data": [
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
      "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
      "environment": "live",
      "billable_metric_key": "chat_message",
      "estimated_units": 500,
      "estimated_cost": 250000,
      "actual_units": null,
      "actual_cost": null,
      "status": "active",
      "expires_at": "2026-07-27T10:30:00Z",
      "metadata": {},
      "created_at": "2026-07-27T10:00:00Z",
      "committed_at": null,
      "released_at": null
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
Response fields
FieldTypeMeaning
datarequiredarray
data.billable_metric_keyrequiredstring

Which billable metric this hold is priced against.

data.estimated_unitsrequiredinteger (int64)

How many units you expect the job to use. QuotaStack prices the hold from this number.

Unit: a count of billable metric units, not credits.

data.estimated_costrequiredinteger (int64)

What the hold costs, at today's price for estimated_units. QuotaStack subtracts this from effective_balance while the reservation stays active.

Unit: millicredits, where 1 credit is 1000 millicredits.

data.actual_unitsoptionalinteger (int64)

How many units the job really used. null until you commit the reservation.

Unit: a count of billable metric units, not credits.

data.actual_costoptionalinteger (int64)

What QuotaStack charged for actual_units. Priced when you commit, which can differ from the rate used to estimate the hold. null until you commit it. Can read higher than what QuotaStack actually collected, if the charge outran both the hold and your free balance.

Unit: millicredits.

data.statusrequiredstring

The hold's current state, right now.

  • activeThe hold is open. Commit it, release it, or wait for it to expire.
  • committedYou committed it. QuotaStack charged actual_units and returned the rest of the hold.
  • releasedYou released it before it was committed. QuotaStack returned the whole hold.
  • expiredNobody closed it in time. QuotaStack's scheduler released the hold on its own.
data.reference_idrequiredstring (uuid)

QuotaStack's own ID for this hold's ledger entries. The hold, the commit, and any release all share it.

data.expires_atrequiredstring (date-time)

When the hold lapses on its own, if nobody commits or releases it first. Set once, from ttl_seconds, at creation.

data.metadatarequiredobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: An empty object is stored instead.

data.committed_atoptionalstring (date-time)

When you committed it. null unless status is committed.

data.released_atoptionalstring (date-time)

When the hold was given back. null unless status is released or expired — committing with actual_units: 0 sets committed_at instead, not this field.

paginationrequiredobject

Cursor-based pagination envelope. Returned on every list endpoint. No total — cursor-only.

See also

List a customer's reservations by external ID

GET/v1/customer-by-external-id/{external_id}/reservations
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This call is the external_id twin of listCustomerReservations. Only the lookup differs. This one is strict, with no auto-create. An unrecognized external_id returns a real 404 here. openapi.yaml does not declare that either; only 200 is listed. An unrecognized status filter value still returns 500, the same undeclared gap the id form has.

Path parameters
FieldTypeMeaning
external_idrequiredstring

Your own ID for this customer, set when you created it. QuotaStack looks up only this value, never the QuotaStack UUID.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Query parameters
FieldTypeMeaning
cursoroptionalstring

Opaque pagination cursor from the previous page's pagination.next_cursor. Omit to start at the first page.

limitoptionalinteger

Page size. Default 20, max 100.

Response 200
{
  "data": [
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a41",
      "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
      "environment": "live",
      "billable_metric_key": "chat_message",
      "estimated_units": 500,
      "estimated_cost": 250000,
      "actual_units": null,
      "actual_cost": null,
      "status": "active",
      "expires_at": "2026-07-27T10:30:00Z",
      "metadata": {},
      "created_at": "2026-07-27T10:00:00Z",
      "committed_at": null,
      "released_at": null
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
Response fields
FieldTypeMeaning
datarequiredarray
data.billable_metric_keyrequiredstring

Which billable metric this hold is priced against.

data.estimated_unitsrequiredinteger (int64)

How many units you expect the job to use. QuotaStack prices the hold from this number.

Unit: a count of billable metric units, not credits.

data.estimated_costrequiredinteger (int64)

What the hold costs, at today's price for estimated_units. QuotaStack subtracts this from effective_balance while the reservation stays active.

Unit: millicredits, where 1 credit is 1000 millicredits.

data.actual_unitsoptionalinteger (int64)

How many units the job really used. null until you commit the reservation.

Unit: a count of billable metric units, not credits.

data.actual_costoptionalinteger (int64)

What QuotaStack charged for actual_units. Priced when you commit, which can differ from the rate used to estimate the hold. null until you commit it. Can read higher than what QuotaStack actually collected, if the charge outran both the hold and your free balance.

Unit: millicredits.

data.statusrequiredstring

The hold's current state, right now.

  • activeThe hold is open. Commit it, release it, or wait for it to expire.
  • committedYou committed it. QuotaStack charged actual_units and returned the rest of the hold.
  • releasedYou released it before it was committed. QuotaStack returned the whole hold.
  • expiredNobody closed it in time. QuotaStack's scheduler released the hold on its own.
data.reference_idrequiredstring (uuid)

QuotaStack's own ID for this hold's ledger entries. The hold, the commit, and any release all share it.

data.expires_atrequiredstring (date-time)

When the hold lapses on its own, if nobody commits or releases it first. Set once, from ttl_seconds, at creation.

data.metadatarequiredobject

Your own key-value tags. QuotaStack stores them unchanged and returns them on every read.

If you leave it out: An empty object is stored instead.

data.committed_atoptionalstring (date-time)

When you committed it. null unless status is committed.

data.released_atoptionalstring (date-time)

When the hold was given back. null unless status is released or expired — committing with actual_units: 0 sets committed_at instead, not this field.

paginationrequiredobject

Cursor-based pagination envelope. Returned on every list endpoint. No total — cursor-only.

See also