quotastack Docs
Docs / API / Credits

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?

10 of 10 operations documented in full.

Get a customer's credit balance (optionally with blocks).

GET/v1/customers/{customer_id}/credits
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. 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.

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
include_blocksoptionalboolean

Include the blocks that make up this balance.

If you leave it out: QuotaStack skips the block list.

Response 200
{
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "balance": 250000,
  "reserved_balance": 50000,
  "pending_balance": 0,
  "effective_balance": 200000,
  "lifetime_earned": 300000,
  "blocks": []
}
Response fields
FieldTypeMeaning
idoptionalstring

QuotaStack credit_account UUID. Empty string when no account row exists yet.

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

balancerequiredinteger (int64)

What the customer has, before holds.

Unit: millicredits, where 1 credit is 1000 millicredits.

reserved_balancerequiredinteger (int64)

Credits held by an open reservation. The customer cannot spend these until the reservation closes.

Unit: millicredits.

pending_balancerequiredinteger (int64)

Credits in a block that starts later, through stack_after. QuotaStack counts them in balance, but not in effective_balance.

Unit: millicredits.

effective_balancerequiredinteger (int64)

What a new charge can actually spend: balance minus reserved_balance minus pending_balance.

Unit: millicredits.

lifetime_earnedrequiredinteger (int64)

Total credits ever granted to this customer. Not reduced when credits are spent.

Unit: millicredits, where 1 credit is 1000 millicredits.

versionrequiredinteger
updated_atoptionalstring (date-time)

Zero value (0001-01-01T00:00:00Z) when no account row exists yet.

blocksrequiredarray

Active credit blocks. Always an array (never null). Empty unless include_blocks=true.

blocks.original_amountrequiredinteger (int64)

How many credits this credit block started with.

Unit: millicredits.

blocks.remaining_amountrequiredinteger (int64)

Credits still left in this credit block.

Unit: millicredits.

Errors

  • 401Missing or invalid authentication.
  • 404No customer here has that ID. A brand new customer still gets a 200. Every balance in it is 0. not-found
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Grant credits to a customer

POST/v1/customers/{customer_id}/credits/grant
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne 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.

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

Body parameters
FieldTypeMeaning
creditsrequiredinteger (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.

sourcerequiredstring

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.

  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • migrationYou moved these from an old system.
reasonrequiredstring

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

When this credit block stops counting toward the balance.

If you leave it out: The block never expires.

duration_secondsoptionalinteger (int64)1–315360000

How long this credit block lasts, counted from when it starts.

If you leave it out: QuotaStack uses expires_at instead. Send neither and the block never expires. Send both and QuotaStack returns 422. The error names duration_seconds, and QuotaStack grants nothing.

stack_afteroptionalobject

Starts this block right after another one ends, instead of right now. QuotaStack finds that other block by matching metadata_match against existing blocks. stack_after requires duration_seconds. Send stack_after without it and QuotaStack returns 422, naming duration_seconds.

If you leave it out: The new block starts now, not after another one.

stack_after.metadata_matchrequiredobject

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.

stack_after.fallbackoptionalstring

What to do when no block matches metadata_match.

If you leave it out: By default, QuotaStack uses now for this.

  • nowStart the new block right now, as if stack_after were not sent.
  • rejectQuotaStack sends back a 409 instead, and grants nothing.
priorityoptionalinteger0–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_keyoptionaldeprecatedstring

QuotaStack throws this field away. Send the value in the Idempotency-Key header instead.

If you leave it out: Nothing changes. The header is what QuotaStack reads.

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
{
  "credits": 100000,
  "source": "promotional",
  "reason": "Welcome bonus",
  "priority": 0
}
Response 201
{
  "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
  }
}
Response fields
FieldTypeMeaning
transactionrequiredobject

The ledger entry this grant wrote.

transaction.deltarequiredinteger (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_unitsoptionalinteger (int64)

Not used for a grant. QuotaStack leaves it empty.

Unit: a count of metric units, not credits.

blockrequiredobject

The credit block this grant created.

block.remaining_amountrequiredinteger (int64)

Credits still available in this block. For a fresh grant, the same as original_amount.

Unit: millicredits.

accountrequiredobject

The account balance right after this grant.

account.reserved_balancerequiredinteger (int64)

Credits held by an open reservation. Unchanged by this grant.

Unit: millicredits.

account.lifetime_earnedrequiredinteger (int64)

Total credits ever granted to this customer. Not reduced when credits are spent.

Unit: millicredits, where 1 credit is 1000 millicredits.

Errors

  • 400The customer_id you sent already belongs to a different tenant. Use your own customer_id, or send an external_id instead. bad-request
  • 401Missing or invalid authentication.
  • 404An unknown customer_id does not trigger this. QuotaStack makes a new customer, and grants the credits to it. not-found
  • 409Another grant or adjustment touched the same account at the same time. Send the request again with the same key. A stack_after grant with fallback: reject and no matching block also returns this. QuotaStack granted nothing in that case. conflict
  • 422A field is missing or wrong. The response names the field. Sending expires_at and duration_seconds together also returns this. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Apply a credit adjustment (positive or negative).

POST/v1/customers/{customer_id}/credits/adjust
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesA 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.

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

Body parameters
FieldTypeMeaning
deltarequiredinteger (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 delta is 0. An omitted field decodes as 0 too.

sourcerequiredstring

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.

  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • migrationYou moved these from an old system.
reasonrequiredstring

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_keyoptionalstring

QuotaStack throws this field away, the same as on a grant. Send the value in the Idempotency-Key header instead.

If you leave it out: Nothing changes. The header is what QuotaStack reads.

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
{
  "delta": -5000,
  "source": "manual",
  "reason": "Correcting a billing error"
}
Response 200
{
  "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
  }
}
Response fields
FieldTypeMeaning
transactionsrequiredarray

One ledger entry per block this adjustment touched. A positive delta always writes exactly one.

transactions.deltarequiredinteger (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_unitsoptionalinteger (int64)

Not used for an adjustment. QuotaStack leaves it empty.

Unit: a count of metric units, not credits.

blockoptionalunknown

The new credit block a positive delta created. QuotaStack sets this to null on a negative delta, since nothing new was created.

accountrequiredobject

The account balance right after this change.

account.reserved_balancerequiredinteger (int64)

Credits held by an open reservation. Unchanged by this adjustment.

Unit: millicredits.

account.lifetime_earnedrequiredinteger (int64)

Total credits ever granted to this customer. Not reduced when credits are spent.

Unit: millicredits, where 1 credit is 1000 millicredits.

Errors

  • 400The customer_id you sent already belongs to a different tenant. Use your own customer_id, or send an external_id instead. bad-request
  • 401Missing or invalid authentication.
  • 404An unknown customer_id does not return this by itself. QuotaStack creates the customer first. A negative delta against that new, empty account returns this instead. There is nothing yet to debit. not-found
  • 409QuotaStack returns this for two reasons. A negative delta more 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-credits
  • 422A field is missing or wrong. The response names the field. A delta of 0 also returns this. An omitted delta decodes as 0 too. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.

List ledger entries for a customer (paginated).

GET/v1/customers/{customer_id}/credits/history
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. 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.

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
typeoptionalstring

Only entries of this kind.

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

  • plan_grantQuotaStack granted this when a subscription started or renewed.
  • topupA customer bought this once, through a topup.
  • consumptionA use of a billable metric caused this, and QuotaStack set the type.
  • reservationQuotaStack held this amount for an open reservation.
  • releaseQuotaStack returned held credit to the balance when a reservation ended.
  • expiryA credit block reached its expiry date, or QuotaStack voided it.
  • adjustmentA person changed the balance by hand, through a grant or an adjustment.
sourceoptionalstring

Only entries with this cause.

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

  • plan_grantQuotaStack set this when a subscription granted the credits.
  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • topupQuotaStack set this when a customer bought credits through a topup.
  • migrationYou moved these from an old system.
billable_metric_keyoptionalstring

Only entries tied to one metric you bill for.

If you leave it out: You get entries for every metric you have.

fromoptionalstring (date-time)

Only entries created at or after this time.

If you leave it out: QuotaStack has no lower bound on time.

tooptionalstring (date-time)

Only entries created before this time.

If you leave it out: QuotaStack has no upper bound on time.

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-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
  }
}
Response fields
FieldTypeMeaning
datarequiredarray

One entry per balance change, newest first.

data.deltarequiredinteger (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_unitsoptionalinteger (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.

paginationrequiredobject

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

Errors

  • 401Missing or invalid authentication.
  • 429Rate limit exceeded.
  • 500Unexpected server error.

See also

Per-metric overage totals for a customer over a period.

GET/v1/customers/{customer_id}/overage
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. 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.

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
fromrequiredstring (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.

torequiredstring (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.

Response 200
{
  "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"
  }
}
Response fields
FieldTypeMeaning
datarequiredarray

Per-billable-metric overage totals, sorted by metric key.

data.overage_mcrequiredinteger (int64)

How much this metric ran over, in this window.

Unit: millicredits.

total_overage_mcrequiredinteger (int64)

The sum of every metric's overage in the window.

periodrequiredobject

The half-open window [from, to) that was queried.

Errors

  • 401Missing or invalid authentication.
  • 404No customer here has that ID. not-found
  • 422from or to is missing, or not a valid timestamp. to must come after from. QuotaStack rejects an equal or earlier value. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Get a customer's credit balance by your external_id.

GET/v1/customer-by-external-id/{external_id}/credits
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. 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.

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
include_blocksoptionalboolean

Include the blocks that make up this balance.

If you leave it out: QuotaStack skips the block list.

Response 200
{
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "balance": 250000,
  "reserved_balance": 50000,
  "pending_balance": 0,
  "effective_balance": 200000,
  "lifetime_earned": 300000,
  "blocks": []
}
Response fields
FieldTypeMeaning
idoptionalstring

QuotaStack credit_account UUID. Empty string when no account row exists yet.

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

balancerequiredinteger (int64)

What the customer has, before holds.

Unit: millicredits, where 1 credit is 1000 millicredits.

reserved_balancerequiredinteger (int64)

Credits held by an open reservation. The customer cannot spend these until the reservation closes.

Unit: millicredits.

pending_balancerequiredinteger (int64)

Credits in a block that starts later, through stack_after. QuotaStack counts them in balance, but not in effective_balance.

Unit: millicredits.

effective_balancerequiredinteger (int64)

What a new charge can actually spend: balance minus reserved_balance minus pending_balance.

Unit: millicredits.

lifetime_earnedrequiredinteger (int64)

Total credits ever granted to this customer. Not reduced when credits are spent.

Unit: millicredits, where 1 credit is 1000 millicredits.

versionrequiredinteger
updated_atoptionalstring (date-time)

Zero value (0001-01-01T00:00:00Z) when no account row exists yet.

blocksrequiredarray

Active credit blocks. Always an array (never null). Empty unless include_blocks=true.

blocks.original_amountrequiredinteger (int64)

How many credits this credit block started with.

Unit: millicredits.

blocks.remaining_amountrequiredinteger (int64)

Credits still left in this credit block.

Unit: millicredits.

Errors

  • 401Missing or invalid authentication.
  • 404No customer here has that external_id. A brand new customer still gets a 200. Every balance in it is 0. not-found
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Grant credits to a customer by external ID

POST/v1/customer-by-external-id/{external_id}/credits/grant
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesOne 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.

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

Body parameters
FieldTypeMeaning
creditsrequiredinteger (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.

sourcerequiredstring

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.

  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • migrationYou moved these from an old system.
reasonrequiredstring

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

When this credit block stops counting toward the balance.

If you leave it out: The block never expires.

duration_secondsoptionalinteger (int64)1–315360000

How long this credit block lasts, counted from when it starts.

If you leave it out: QuotaStack uses expires_at instead. Send neither and the block never expires. Send both and QuotaStack returns 422. The error names duration_seconds, and QuotaStack grants nothing.

stack_afteroptionalobject

Starts this block right after another one ends, instead of right now. QuotaStack finds that other block by matching metadata_match against existing blocks. stack_after requires duration_seconds. Send stack_after without it and QuotaStack returns 422, naming duration_seconds.

If you leave it out: The new block starts now, not after another one.

stack_after.metadata_matchrequiredobject

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.

stack_after.fallbackoptionalstring

What to do when no block matches metadata_match.

If you leave it out: By default, QuotaStack uses now for this.

  • nowStart the new block right now, as if stack_after were not sent.
  • rejectQuotaStack sends back a 409 instead, and grants nothing.
priorityoptionalinteger0–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_keyoptionaldeprecatedstring

QuotaStack throws this field away. Send the value in the Idempotency-Key header instead.

If you leave it out: Nothing changes. The header is what QuotaStack reads.

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
{
  "credits": 100000,
  "source": "promotional",
  "reason": "Welcome bonus",
  "priority": 0
}
Response 201
{
  "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
  }
}
Response fields
FieldTypeMeaning
transactionrequiredobject

The ledger entry this grant wrote.

transaction.deltarequiredinteger (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_unitsoptionalinteger (int64)

Not used for a grant. QuotaStack leaves it empty.

Unit: a count of metric units, not credits.

blockrequiredobject

The credit block this grant created.

block.remaining_amountrequiredinteger (int64)

Credits still available in this block. For a fresh grant, the same as original_amount.

Unit: millicredits.

accountrequiredobject

The account balance right after this grant.

account.reserved_balancerequiredinteger (int64)

Credits held by an open reservation. Unchanged by this grant.

Unit: millicredits.

account.lifetime_earnedrequiredinteger (int64)

Total credits ever granted to this customer. Not reduced when credits are spent.

Unit: millicredits, where 1 credit is 1000 millicredits.

Errors

  • 400Malformed request.
  • 401Missing or invalid authentication.
  • 404No customer here has that external_id. not-found
  • 409Another grant or adjustment touched the same account at the same time. Send the request again with the same key. A stack_after grant with fallback: reject and no matching block also returns this. QuotaStack granted nothing in that case. conflict
  • 422A field is missing or wrong. The response names the field. Sending expires_at and duration_seconds together also returns this. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.

Apply a credit adjustment by your external_id.

POST/v1/customer-by-external-id/{external_id}/credits/adjust
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesA 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.

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

Body parameters
FieldTypeMeaning
deltarequiredinteger (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 delta is 0. An omitted field decodes as 0 too.

sourcerequiredstring

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.

  • promotionalA free gift. Maybe a welcome bonus, or an ad you ran.
  • compensationYou are making something right. An outage, or a support case.
  • referralA customer sent a friend your way.
  • manualA person added these by hand.
  • migrationYou moved these from an old system.
reasonrequiredstring

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_keyoptionalstring

QuotaStack throws this field away, the same as on a grant. Send the value in the Idempotency-Key header instead.

If you leave it out: Nothing changes. The header is what QuotaStack reads.

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
{
  "delta": -5000,
  "source": "manual",
  "reason": "Correcting a billing error"
}
Response 200
{
  "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
  }
}
Response fields
FieldTypeMeaning
transactionsrequiredarray

One ledger entry per block this adjustment touched. A positive delta always writes exactly one.

transactions.deltarequiredinteger (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_unitsoptionalinteger (int64)

Not used for an adjustment. QuotaStack leaves it empty.

Unit: a count of metric units, not credits.

blockoptionalunknown

The new credit block a positive delta created. QuotaStack sets this to null on a negative delta, since nothing new was created.

accountrequiredobject

The account balance right after this change.

account.reserved_balancerequiredinteger (int64)

Credits held by an open reservation. Unchanged by this adjustment.

Unit: millicredits.

account.lifetime_earnedrequiredinteger (int64)

Total credits ever granted to this customer. Not reduced when credits are spent.

Unit: millicredits, where 1 credit is 1000 millicredits.

Errors

  • 400Malformed request.
  • 401Missing or invalid authentication.
  • 404No customer here has that external_id. A customer with no credit yet gets this too, if delta is negative. not-found
  • 409QuotaStack returns this for two reasons. A negative delta more 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-credits
  • 422A field is missing or wrong. The response names the field. A delta of 0 also returns this. An omitted delta decodes as 0 too. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.

List ledger entries by your external_id.

GET/v1/customer-by-external-id/{external_id}/credits/history
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. 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.

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-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
  }
}
Response fields
FieldTypeMeaning
datarequiredarray

One entry per balance change, newest first.

data.deltarequiredinteger (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_unitsoptionalinteger (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.

paginationrequiredobject

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

Errors

  • 401Missing or invalid authentication.
  • 429Rate limit exceeded.
  • 500Unexpected server error.

See also

Per-metric overage totals by your external_id over a period.

GET/v1/customer-by-external-id/{external_id}/overage
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. 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.

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
fromrequiredstring (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.

torequiredstring (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.

Response 200
{
  "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"
  }
}
Response fields
FieldTypeMeaning
datarequiredarray

Per-billable-metric overage totals, sorted by metric key.

data.overage_mcrequiredinteger (int64)

How much this metric ran over, in this window.

Unit: millicredits.

total_overage_mcrequiredinteger (int64)

The sum of every metric's overage in the window.

periodrequiredobject

The half-open window [from, to) that was queried.

Errors

  • 401Missing or invalid authentication.
  • 404No customer here has that external_id. not-found
  • 422from or to is missing, or not a valid timestamp. to must come after from. QuotaStack rejects an equal or earlier value. validation-error
  • 429Rate limit exceeded.
  • 500Unexpected server error.