Credits
What a customer spends. QuotaStack tracks a balance for each one.
Credits are what a customer spends. QuotaStack keeps a balance for every customer, in every environment.
A balance is not one number in a row. A balance is the sum of every active credit block. Each block carries its own expiry, its own priority, and its own record of where it came from.
You add credits with a grant. You spend them by recording usage. QuotaStack writes every change to a ledger that only ever grows.
Which call do I want?
- You have QuotaStack's customer ID →
POST /v1/customers/{customer_id}/credits/grant - You have your own user ID →
POST /v1/customer-by-external-id/{external_id}/credits/grant - You want to read a balance and you have your own user ID →
GET /v1/customer-by-external-id/{external_id}/creditsOn a valid ID, both grant endpoints do the same thing. You do not need to fetch the QuotaStack ID first. On an unknown ID, the two forms differ. The QuotaStack-ID form makes a new customer and grants to it. Theexternal_idform returns a404instead.
Get a customer's credit balance (optionally with blocks).
/v1/customers/{customer_id}/credits | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. A read has no side effects. |
| Fires | No webhooks. |
A balance is the sum of every active credit block for one customer. QuotaStack returns one snapshot of the total. Add include_blocks=true to also see the blocks behind it.
| Field | Type | Meaning |
|---|---|---|
customer_idrequired | string | Customer identifier. Can be either a QuotaStack UUID or your own |
| Field | Type | Meaning |
|---|---|---|
include_blocksoptional | boolean | Include the blocks that make up this balance. If you leave it out: QuotaStack skips the block list. |
{
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"balance": 250000,
"reserved_balance": 50000,
"pending_balance": 0,
"effective_balance": 200000,
"lifetime_earned": 300000,
"blocks": []
} | Field | Type | Meaning |
|---|---|---|
idoptional | string | QuotaStack credit_account UUID. Empty string when no account row exists yet. |
tenant_idrequired | string (uuid) | |
customer_idrequired | string (uuid) | |
environmentrequired | string |
Which environment this resource lives in. Determined by the API key prefix used ( |
balancerequired | integer (int64) | What the customer has, before holds. Unit: millicredits, where 1 credit is 1000 millicredits. |
reserved_balancerequired | integer (int64) | Credits held by an open reservation. The customer cannot spend these until the reservation closes. Unit: millicredits. |
pending_balancerequired | integer (int64) | Credits in a block that starts later, through Unit: millicredits. |
effective_balancerequired | integer (int64) | What a new charge can actually spend: Unit: millicredits. |
lifetime_earnedrequired | integer (int64) | Total credits ever granted to this customer. Not reduced when credits are spent. Unit: millicredits, where 1 credit is 1000 millicredits. |
versionrequired | integer | |
updated_atoptional | string (date-time) | Zero value ( |
blocksrequired | array | Active credit blocks. Always an array (never null). Empty unless |
blocks.original_amountrequired | integer (int64) | How many credits this credit block started with. Unit: millicredits. |
blocks.remaining_amountrequired | integer (int64) | Credits still left in this credit block. Unit: millicredits. |
Errors
401— Missing or invalid authentication.404— No customer here has that ID. A brand new customer still gets a200. Every balance in it is 0. not-found429— Rate limit exceeded.500— Unexpected server error.
See also
Grant credits to a customer
/v1/customers/{customer_id}/credits/grant | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | One new credit block and one new ledger entry. |
| Fires | credit.granted |
A grant adds credits to one customer. QuotaStack creates a new credit block and writes one ledger entry. QuotaStack never edits an existing block. An unknown customer_id is not an error. QuotaStack creates a new customer instead. The value you sent becomes its external_id, not its id. QuotaStack assigns it a fresh id. Look it up again with the same value, on either path form, and QuotaStack finds it.
| 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_idrequired | string | Customer identifier. Can be either a QuotaStack UUID or your own |
| Field | Type | Meaning |
|---|---|---|
creditsrequired | integer (int64)1–∞ | How many credits to add. Must be more than zero. Unit: millicredits, where 1 credit is 1000 millicredits. If you leave it out: QuotaStack rejects the call. This field is required. |
sourcerequired | string | Why you are adding these credits. QuotaStack stores it on the block and in the audit log. If you leave it out: QuotaStack rejects the call. This field is required.
|
reasonrequired | string | Your own words about this grant. QuotaStack shows it in the audit log. If you leave it out: QuotaStack rejects the call. This field is required. |
expires_atoptional | string (date-time) | When this credit block stops counting toward the balance. If you leave it out: The block never expires. |
duration_secondsoptional | integer (int64)1–315360000 | How long this credit block lasts, counted from when it starts. If you leave it out: QuotaStack uses |
stack_afteroptional | object | Starts this block right after another one ends, instead of right now. QuotaStack finds that other block by matching If you leave it out: The new block starts now, not after another one. |
stack_after.metadata_matchrequired | object | Key-value tags to match against a block. QuotaStack picks the one with the latest expiry as the anchor. If you leave it out: QuotaStack rejects the call. This field is required whenever you send |
stack_after.fallbackoptional | string | What to do when no block matches If you leave it out: By default, QuotaStack uses
|
priorityoptional | integer0–255 | Which credit block gets spent first. A lower number goes first. Within the same number, QuotaStack spends the block closest to expiring first. If you leave it out: By default, it is 0. |
idempotency_keyoptionaldeprecated | string | QuotaStack throws this field away. Send the value in the If you leave it out: Nothing changes. The header is what QuotaStack reads. |
metadataoptional | object | 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. |
{
"credits": 100000,
"source": "promotional",
"reason": "Welcome bonus",
"priority": 0
} {
"transaction": {
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"delta": 100000,
"type": "adjustment",
"source": "promotional",
"credit_block_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
"created_at": "2026-07-27T10:00:00Z"
},
"block": {
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"original_amount": 100000,
"remaining_amount": 100000,
"source": "promotional",
"priority": 0,
"expires_at": null,
"created_at": "2026-07-27T10:00:00Z"
},
"account": {
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"balance": 100000,
"reserved_balance": 0,
"lifetime_earned": 100000,
"version": 1
}
} | Field | Type | Meaning |
|---|---|---|
transactionrequired | object | The ledger entry this grant wrote. |
transaction.deltarequired | integer (int64) | How much this entry changed the balance by. Positive for a credit; negative for a debit. Unit: millicredits, where 1 credit is 1000 millicredits. |
transaction.cost_unitsoptional | integer (int64) | Not used for a grant. QuotaStack leaves it empty. Unit: a count of metric units, not credits. |
blockrequired | object | The credit block this grant created. |
block.remaining_amountrequired | integer (int64) | Credits still available in this block. For a fresh grant, the same as Unit: millicredits. |
accountrequired | object | The account balance right after this grant. |
account.reserved_balancerequired | integer (int64) | Credits held by an open reservation. Unchanged by this grant. Unit: millicredits. |
account.lifetime_earnedrequired | integer (int64) | Total credits ever granted to this customer. Not reduced when credits are spent. Unit: millicredits, where 1 credit is 1000 millicredits. |
Errors
400— Thecustomer_idyou sent already belongs to a different tenant. Use your owncustomer_id, or send anexternal_idinstead. bad-request401— Missing or invalid authentication.404— An unknowncustomer_iddoes not trigger this. QuotaStack makes a new customer, and grants the credits to it. not-found409— Another grant or adjustment touched the same account at the same time. Send the request again with the same key. Astack_aftergrant withfallback: rejectand no matching block also returns this. QuotaStack granted nothing in that case. conflict422— A field is missing or wrong. The response names the field. Sendingexpires_atandduration_secondstogether also returns this. validation-error429— Rate limit exceeded.500— Unexpected server error.
Apply a credit adjustment (positive or negative).
/v1/customers/{customer_id}/credits/adjust | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | A positive delta writes one new credit block and one new ledger entry, like a grant. A negative delta writes one entry per block it drains, and creates no new block. |
| Fires | credit.granted, credit.consumed, credit.low_balance, credit.exhausted |
An adjustment changes a customer's balance by a signed amount, called delta. QuotaStack grants credit when delta is a positive number, the same as grantCustomerCredits. But the new block always gets priority 0, and never expires. Use grantCustomerCredits if you need to set either one. QuotaStack takes credit away when delta is negative, spending down blocks in order. QuotaStack may write more than one ledger entry for one debit, one per block it drains from.
| 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_idrequired | string | Customer identifier. Can be either a QuotaStack UUID or your own |
| Field | Type | Meaning |
|---|---|---|
deltarequired | integer (int64) | How much to change the balance by. A positive number grants credits; a negative number debits them. Unit: millicredits, where 1 credit is 1000 millicredits. If you leave it out: QuotaStack rejects the call if |
sourcerequired | string | Why you are changing the balance. QuotaStack stores it on the ledger entry and in the audit log. If you leave it out: QuotaStack rejects the call. This field is required.
|
reasonrequired | string | Your own words about this adjustment. QuotaStack shows it in the audit log. If you leave it out: QuotaStack rejects the call. This field is required. |
idempotency_keyoptional | string | QuotaStack throws this field away, the same as on a grant. Send the value in the If you leave it out: Nothing changes. The header is what QuotaStack reads. |
metadataoptional | object | 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. |
{
"delta": -5000,
"source": "manual",
"reason": "Correcting a billing error"
} {
"transactions": [
{
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"delta": -5000,
"type": "adjustment",
"credit_block_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
"created_at": "2026-07-27T10:05:00Z"
}
],
"block": null,
"account": {
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"balance": 95000,
"reserved_balance": 0,
"lifetime_earned": 100000,
"version": 2
}
} | Field | Type | Meaning |
|---|---|---|
transactionsrequired | array | One ledger entry per block this adjustment touched. A positive |
transactions.deltarequired | integer (int64) | How much this entry changed the balance by. Positive for a credit; negative for a debit. Unit: millicredits, where 1 credit is 1000 millicredits. |
transactions.cost_unitsoptional | integer (int64) | Not used for an adjustment. QuotaStack leaves it empty. Unit: a count of metric units, not credits. |
blockoptional | unknown | The new credit block a positive |
accountrequired | object | The account balance right after this change. |
account.reserved_balancerequired | integer (int64) | Credits held by an open reservation. Unchanged by this adjustment. Unit: millicredits. |
account.lifetime_earnedrequired | integer (int64) | Total credits ever granted to this customer. Not reduced when credits are spent. Unit: millicredits, where 1 credit is 1000 millicredits. |
Errors
400— Thecustomer_idyou sent already belongs to a different tenant. Use your owncustomer_id, or send anexternal_idinstead. bad-request401— Missing or invalid authentication.404— An unknowncustomer_iddoes not return this by itself. QuotaStack creates the customer first. A negativedeltaagainst that new, empty account returns this instead. There is nothing yet to debit. not-found409— QuotaStack returns this for two reasons. A negativedeltamore than the balance can cover is one. QuotaStack changes nothing in that case. A concurrent grant or adjustment on the same account is the other. Retry with the same key for that one. insufficient-credits422— A field is missing or wrong. The response names the field. Adeltaof 0 also returns this. An omitteddeltadecodes as 0 too. validation-error429— Rate limit exceeded.500— Unexpected server error.
See also
List ledger entries for a customer (paginated).
/v1/customers/{customer_id}/credits/history | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. A read has no side effects. |
| Fires | No webhooks. |
This endpoint lists every ledger entry for one customer, newest first. Filter by type, source, billable_metric_key, or a date range; each one is optional. An unknown customer gets a 404. A bad from or to gets a 400.
| Field | Type | Meaning |
|---|---|---|
customer_idrequired | string | Customer identifier. Can be either a QuotaStack UUID or your own |
| Field | Type | Meaning |
|---|---|---|
typeoptional | string | Only entries of this kind. If you leave it out: Every kind comes back, with no filter.
|
sourceoptional | string | Only entries with this cause. If you leave it out: Every cause comes back, with no filter.
|
billable_metric_keyoptional | string | Only entries tied to one metric you bill for. If you leave it out: You get entries for every metric you have. |
fromoptional | string (date-time) | Only entries created at or after this time. If you leave it out: QuotaStack has no lower bound on time. |
tooptional | string (date-time) | Only entries created before this time. If you leave it out: QuotaStack has no upper bound on time. |
cursoroptional | string | Opaque pagination cursor from the previous page's |
limitoptional | integer | Page size. Default 20, max 100. |
{
"data": [
{
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"delta": -5000,
"type": "consumption",
"billable_metric_key": "chat_message",
"cost_units": 5,
"created_at": "2026-07-27T10:05:00Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
} | Field | Type | Meaning |
|---|---|---|
datarequired | array | One entry per balance change, newest first. |
data.deltarequired | integer (int64) | How much this entry changed the balance by. Positive for a credit; negative for a debit. Unit: millicredits, where 1 credit is 1000 millicredits. |
data.cost_unitsoptional | integer (int64) | How much of a metric this entry used. Empty unless it comes from a billable metric. Unit: a count of metric units, not credits. |
paginationrequired | object | Cursor-based pagination envelope. Returned on every list endpoint. No |
Errors
401— Missing or invalid authentication.429— Rate limit exceeded.500— Unexpected server error.
See also
Per-metric overage totals for a customer over a period.
/v1/customers/{customer_id}/overage | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. A read has no side effects. |
| Fires | No webhooks. |
Overage is credit spent past the balance, under an allow or notify rule. This endpoint adds it up by billable metric, over one time window. QuotaStack never makes a customer for a read. An unknown customer gets a 404.
| Field | Type | Meaning |
|---|---|---|
customer_idrequired | string | Customer identifier. Can be either a QuotaStack UUID or your own |
| Field | Type | Meaning |
|---|---|---|
fromrequired | string (date-time) | Return entries made at this moment or later. An RFC 3339 timestamp. If you leave it out: You start at the oldest entry. |
torequired | string (date-time) | Return entries made before this moment. The moment itself is left out. If you leave it out: You run up to the newest entry. |
{
"data": [
{
"billable_metric_key": "chat_message",
"overage_mc": 5000
}
],
"total_overage_mc": 5000,
"period": {
"from": "2026-07-01T00:00:00Z",
"to": "2026-08-01T00:00:00Z"
}
} | Field | Type | Meaning |
|---|---|---|
datarequired | array | Per-billable-metric overage totals, sorted by metric key. |
data.overage_mcrequired | integer (int64) | How much this metric ran over, in this window. Unit: millicredits. |
total_overage_mcrequired | integer (int64) | The sum of every metric's overage in the window. |
periodrequired | object | The half-open window |
Errors
401— Missing or invalid authentication.404— No customer here has that ID. not-found422—fromortois missing, or not a valid timestamp.tomust come afterfrom. QuotaStack rejects an equal or earlier value. validation-error429— Rate limit exceeded.500— Unexpected server error.
See also
Get a customer's credit balance by your external_id.
/v1/customer-by-external-id/{external_id}/credits | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. A read has no side effects. |
| Fires | No webhooks. |
A balance is the sum of every active credit block for one customer, looked up by your own external_id. QuotaStack returns one snapshot of the total. Add include_blocks=true to also see the blocks behind it.
| Field | Type | Meaning |
|---|---|---|
external_idrequired | string | 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. |
| Field | Type | Meaning |
|---|---|---|
include_blocksoptional | boolean | Include the blocks that make up this balance. If you leave it out: QuotaStack skips the block list. |
{
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"balance": 250000,
"reserved_balance": 50000,
"pending_balance": 0,
"effective_balance": 200000,
"lifetime_earned": 300000,
"blocks": []
} | Field | Type | Meaning |
|---|---|---|
idoptional | string | QuotaStack credit_account UUID. Empty string when no account row exists yet. |
tenant_idrequired | string (uuid) | |
customer_idrequired | string (uuid) | |
environmentrequired | string |
Which environment this resource lives in. Determined by the API key prefix used ( |
balancerequired | integer (int64) | What the customer has, before holds. Unit: millicredits, where 1 credit is 1000 millicredits. |
reserved_balancerequired | integer (int64) | Credits held by an open reservation. The customer cannot spend these until the reservation closes. Unit: millicredits. |
pending_balancerequired | integer (int64) | Credits in a block that starts later, through Unit: millicredits. |
effective_balancerequired | integer (int64) | What a new charge can actually spend: Unit: millicredits. |
lifetime_earnedrequired | integer (int64) | Total credits ever granted to this customer. Not reduced when credits are spent. Unit: millicredits, where 1 credit is 1000 millicredits. |
versionrequired | integer | |
updated_atoptional | string (date-time) | Zero value ( |
blocksrequired | array | Active credit blocks. Always an array (never null). Empty unless |
blocks.original_amountrequired | integer (int64) | How many credits this credit block started with. Unit: millicredits. |
blocks.remaining_amountrequired | integer (int64) | Credits still left in this credit block. Unit: millicredits. |
Errors
401— Missing or invalid authentication.404— No customer here has thatexternal_id. A brand new customer still gets a200. Every balance in it is 0. not-found429— Rate limit exceeded.500— Unexpected server error.
See also
Grant credits to a customer by external ID
/v1/customer-by-external-id/{external_id}/credits/grant | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | One new credit block and one new ledger entry. |
| Fires | credit.granted |
A grant adds credits to one customer, looked up by your own external_id. QuotaStack creates a new credit block and writes one ledger entry, exactly like grantCustomerCredits. QuotaStack never edits an existing block.
| 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 |
|---|---|---|
external_idrequired | string | 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. |
| Field | Type | Meaning |
|---|---|---|
creditsrequired | integer (int64)1–∞ | How many credits to add. Must be more than zero. Unit: millicredits, where 1 credit is 1000 millicredits. If you leave it out: QuotaStack rejects the call. This field is required. |
sourcerequired | string | Why you are adding these credits. QuotaStack stores it on the block and in the audit log. If you leave it out: QuotaStack rejects the call. This field is required.
|
reasonrequired | string | Your own words about this grant. QuotaStack shows it in the audit log. If you leave it out: QuotaStack rejects the call. This field is required. |
expires_atoptional | string (date-time) | When this credit block stops counting toward the balance. If you leave it out: The block never expires. |
duration_secondsoptional | integer (int64)1–315360000 | How long this credit block lasts, counted from when it starts. If you leave it out: QuotaStack uses |
stack_afteroptional | object | Starts this block right after another one ends, instead of right now. QuotaStack finds that other block by matching If you leave it out: The new block starts now, not after another one. |
stack_after.metadata_matchrequired | object | Key-value tags to match against a block. QuotaStack picks the one with the latest expiry as the anchor. If you leave it out: QuotaStack rejects the call. This field is required whenever you send |
stack_after.fallbackoptional | string | What to do when no block matches If you leave it out: By default, QuotaStack uses
|
priorityoptional | integer0–255 | Which credit block gets spent first. A lower number goes first. Within the same number, QuotaStack spends the block closest to expiring first. If you leave it out: By default, it is 0. |
idempotency_keyoptionaldeprecated | string | QuotaStack throws this field away. Send the value in the If you leave it out: Nothing changes. The header is what QuotaStack reads. |
metadataoptional | object | 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. |
{
"credits": 100000,
"source": "promotional",
"reason": "Welcome bonus",
"priority": 0
} {
"transaction": {
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"delta": 100000,
"type": "adjustment",
"source": "promotional",
"credit_block_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
"created_at": "2026-07-27T10:00:00Z"
},
"block": {
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"original_amount": 100000,
"remaining_amount": 100000,
"source": "promotional",
"priority": 0,
"expires_at": null,
"created_at": "2026-07-27T10:00:00Z"
},
"account": {
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"balance": 100000,
"reserved_balance": 0,
"lifetime_earned": 100000,
"version": 1
}
} | Field | Type | Meaning |
|---|---|---|
transactionrequired | object | The ledger entry this grant wrote. |
transaction.deltarequired | integer (int64) | How much this entry changed the balance by. Positive for a credit; negative for a debit. Unit: millicredits, where 1 credit is 1000 millicredits. |
transaction.cost_unitsoptional | integer (int64) | Not used for a grant. QuotaStack leaves it empty. Unit: a count of metric units, not credits. |
blockrequired | object | The credit block this grant created. |
block.remaining_amountrequired | integer (int64) | Credits still available in this block. For a fresh grant, the same as Unit: millicredits. |
accountrequired | object | The account balance right after this grant. |
account.reserved_balancerequired | integer (int64) | Credits held by an open reservation. Unchanged by this grant. Unit: millicredits. |
account.lifetime_earnedrequired | integer (int64) | Total credits ever granted to this customer. Not reduced when credits are spent. Unit: millicredits, where 1 credit is 1000 millicredits. |
Errors
400— Malformed request.401— Missing or invalid authentication.404— No customer here has thatexternal_id. not-found409— Another grant or adjustment touched the same account at the same time. Send the request again with the same key. Astack_aftergrant withfallback: rejectand no matching block also returns this. QuotaStack granted nothing in that case. conflict422— A field is missing or wrong. The response names the field. Sendingexpires_atandduration_secondstogether also returns this. validation-error429— Rate limit exceeded.500— Unexpected server error.
Apply a credit adjustment by your external_id.
/v1/customer-by-external-id/{external_id}/credits/adjust | Idempotency-Key | Required. Returns 422 without it. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | A positive delta writes one new credit block and one new ledger entry, like a grant. A negative delta writes one entry per block it drains, and creates no new block. |
| Fires | credit.granted, credit.consumed, credit.low_balance, credit.exhausted |
An adjustment changes a customer's balance by a signed amount, called delta, looked up by your own external_id. QuotaStack grants credit when delta is a positive number, the same as grantCustomerCreditsByExternalId. But the new block always gets priority 0, and never expires. Use grantCustomerCreditsByExternalId if you need to set either one. QuotaStack takes credit away when delta is negative, spending down blocks in order. QuotaStack may write more than one ledger entry for one debit, one per block it drains from.
| 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 |
|---|---|---|
external_idrequired | string | 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. |
| Field | Type | Meaning |
|---|---|---|
deltarequired | integer (int64) | How much to change the balance by. A positive number grants credits; a negative number debits them. Unit: millicredits, where 1 credit is 1000 millicredits. If you leave it out: QuotaStack rejects the call if |
sourcerequired | string | Why you are changing the balance. QuotaStack stores it on the ledger entry and in the audit log. If you leave it out: QuotaStack rejects the call. This field is required.
|
reasonrequired | string | Your own words about this adjustment. QuotaStack shows it in the audit log. If you leave it out: QuotaStack rejects the call. This field is required. |
idempotency_keyoptional | string | QuotaStack throws this field away, the same as on a grant. Send the value in the If you leave it out: Nothing changes. The header is what QuotaStack reads. |
metadataoptional | object | 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. |
{
"delta": -5000,
"source": "manual",
"reason": "Correcting a billing error"
} {
"transactions": [
{
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"delta": -5000,
"type": "adjustment",
"credit_block_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a31",
"created_at": "2026-07-27T10:05:00Z"
}
],
"block": null,
"account": {
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"balance": 95000,
"reserved_balance": 0,
"lifetime_earned": 100000,
"version": 2
}
} | Field | Type | Meaning |
|---|---|---|
transactionsrequired | array | One ledger entry per block this adjustment touched. A positive |
transactions.deltarequired | integer (int64) | How much this entry changed the balance by. Positive for a credit; negative for a debit. Unit: millicredits, where 1 credit is 1000 millicredits. |
transactions.cost_unitsoptional | integer (int64) | Not used for an adjustment. QuotaStack leaves it empty. Unit: a count of metric units, not credits. |
blockoptional | unknown | The new credit block a positive |
accountrequired | object | The account balance right after this change. |
account.reserved_balancerequired | integer (int64) | Credits held by an open reservation. Unchanged by this adjustment. Unit: millicredits. |
account.lifetime_earnedrequired | integer (int64) | Total credits ever granted to this customer. Not reduced when credits are spent. Unit: millicredits, where 1 credit is 1000 millicredits. |
Errors
400— Malformed request.401— Missing or invalid authentication.404— No customer here has thatexternal_id. A customer with no credit yet gets this too, ifdeltais negative. not-found409— QuotaStack returns this for two reasons. A negativedeltamore than the balance can cover is one. QuotaStack changes nothing in that case. A concurrent grant or adjustment on the same account is the other. Retry with the same key for that one. insufficient-credits422— A field is missing or wrong. The response names the field. Adeltaof 0 also returns this. An omitteddeltadecodes as 0 too. validation-error429— Rate limit exceeded.500— Unexpected server error.
See also
List ledger entries by your external_id.
/v1/customer-by-external-id/{external_id}/credits/history | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. A read has no side effects. |
| Fires | No webhooks. |
This endpoint is the external_id twin of listCustomerCreditsHistory, newest first too. The same type, source, billable_metric_key, from, and to filters work here too, though they are not listed below. An unknown external_id gets a 404. A bad from or to gets a 400.
| Field | Type | Meaning |
|---|---|---|
external_idrequired | string | 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. |
| Field | Type | Meaning |
|---|---|---|
cursoroptional | string | Opaque pagination cursor from the previous page's |
limitoptional | integer | Page size. Default 20, max 100. |
{
"data": [
{
"id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"delta": -5000,
"type": "consumption",
"billable_metric_key": "chat_message",
"cost_units": 5,
"created_at": "2026-07-27T10:05:00Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
} | Field | Type | Meaning |
|---|---|---|
datarequired | array | One entry per balance change, newest first. |
data.deltarequired | integer (int64) | How much this entry changed the balance by. Positive for a credit; negative for a debit. Unit: millicredits, where 1 credit is 1000 millicredits. |
data.cost_unitsoptional | integer (int64) | How much of a metric this entry used. Empty unless it comes from a billable metric. Unit: a count of metric units, not credits. |
paginationrequired | object | Cursor-based pagination envelope. Returned on every list endpoint. No |
Errors
401— Missing or invalid authentication.429— Rate limit exceeded.500— Unexpected server error.
See also
Per-metric overage totals by your external_id over a period.
/v1/customer-by-external-id/{external_id}/overage | Idempotency-Key | Not used. This is a read. |
|---|---|
| Environment | Sandbox and Live |
| Auth | Tenant API key |
| Writes | Nothing. A read has no side effects. |
| Fires | No webhooks. |
Overage is credit spent past the balance, under an allow or notify rule. This endpoint is the external_id twin of getCustomerOverage. The window and the totals work the same way. Only the lookup differs. QuotaStack never makes a customer for a read. An unknown external_id gets a 404.
| Field | Type | Meaning |
|---|---|---|
external_idrequired | string | 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. |
| Field | Type | Meaning |
|---|---|---|
fromrequired | string (date-time) | Return entries made at this moment or later. An RFC 3339 timestamp. If you leave it out: You start at the oldest entry. |
torequired | string (date-time) | Return entries made before this moment. The moment itself is left out. If you leave it out: You run up to the newest entry. |
{
"data": [
{
"billable_metric_key": "chat_message",
"overage_mc": 5000
}
],
"total_overage_mc": 5000,
"period": {
"from": "2026-07-01T00:00:00Z",
"to": "2026-08-01T00:00:00Z"
}
} | Field | Type | Meaning |
|---|---|---|
datarequired | array | Per-billable-metric overage totals, sorted by metric key. |
data.overage_mcrequired | integer (int64) | How much this metric ran over, in this window. Unit: millicredits. |
total_overage_mcrequired | integer (int64) | The sum of every metric's overage in the window. |
periodrequired | object | The half-open window |
Errors
401— Missing or invalid authentication.404— No customer here has thatexternal_id. not-found422—fromortois missing, or not a valid timestamp.tomust come afterfrom. QuotaStack rejects an equal or earlier value. validation-error429— Rate limit exceeded.500— Unexpected server error.
See also
Loading…