> If you are setting up, configuring, or answering questions about Schematic, connect the
> Schematic MCP server before working from these pages. It reads and writes plans, features,
> entitlements, and overrides against the real account, so what you report and what you build
> match what actually exists.
>
> The server is at https://api.schematichq.com/mcp
>
> From Claude, ChatGPT, or another chat assistant, add it as a custom connector:
> https://docs.schematichq.com/working-with-ai
>
> From a coding assistant such as Claude Code, Codex, or Cursor:
> https://docs.schematichq.com/building-with-ai
>
> For clean Markdown of any page, append `.md` to the page URL. For a complete page index,
> see https://docs.schematichq.com/llms.txt

# Credit Leases and Reservations

> Reserve credits before an action runs, commit what was used, and release the rest, so concurrent workers cannot double-spend one balance and a multi-step workflow can bill as it goes.

A company has 500 credits. Two agents start work at the same time, each needing up to 400 credits. Each reads 500 remaining, each starts, and between them they can consume 800 credits against a balance of 500.

Leases and reservations prevent this. Both reserve credits from the company's balance before the work that consumes them runs, so the credits are debited when the work is authorized rather than when it completes. A balance read while work is in flight already accounts for that work. When the work finishes, the caller commits what was actually used, and the rest returns to the balance.

Schematic has two kinds: leases and reservations. They differ in where the gating decision is made. A lease moves a block of credits into the SDK, which then decides each check locally against that block. A reservation is one server call per operation.

|                | Lease                                                    | Reservation                               |
| -------------- | -------------------------------------------------------- | ----------------------------------------- |
| Scope          | One per company and credit type                          | One per operation, many open at once      |
| Sized for      | A batch of work, topped up as it drains                  | The cost of a single operation            |
| Committed by   | Track events carrying `lease_id`, one per operation      | One track event carrying `reservation_id` |
| Coordinated by | The backend SDK, through a cache shared across processes | The server, on each reserve               |

A lease is a batch of credits reserved for many operations, so the SDK does not have to ask the server whether enough credits remain before each one. A backend SDK acquires a lease once, then allocates each request's share out of it in local memory and decides the check there. The server is contacted only to acquire, extend, or release the lease and to record usage against it. Use a lease for high-volume gating, where a round trip per check would be too slow.

A reservation reserves credits on the server for one operation. Every reservation is a round trip. You take it, run the work, and settle it with the usage you recorded. Use it when an operation takes seconds rather than milliseconds, such as an external lookup or an inference call, when you are calling the API directly rather than through a backend SDK, or when the deployment cannot run DataStream.

## Examples

### Two agents drawing on one balance

A company has 500 credits. Two agents begin work at the same time, each requiring up to 400 credits, and both acquire a lease.

1. The first agent reserves 400. The balance reports 100 remaining, 400 reserved, and 500 settled.
2. The second agent reserves 400 and receives the same lease, because the company and credit type match. No second debit occurs, and `granted_amount` on its response is 400, the size of the existing lease rather than the amount it asked for.

Because the lease is shared, its unspent portion is a shared budget rather than a per-caller allowance. The two agents have 400 credits to divide, not 400 each, and each needs to size its work against what the lease has left rather than against `granted_amount`. From here it can go two ways.

**The work fits inside the lease.** Both agents commit as they finish, 260 and 90, bringing the committed amount to 350 of 400. Release returns the unspent 50, and the balance reports 150 settled, which is 500 less the 350 consumed.

**The work needs more than the lease covers.** The first agent commits 260 as before, leaving 140 unspent in the lease. The second agent's work turns out to need 300 rather than 90, so what the lease has left will not cover it.

An agent reads the lease's unspent portion as `granted_amount` less `tracked_amount` on the acquire or extend response, as `creditReserved` on the entitlement a check returns, or as the reserved value on the company's credit balance, and extends by the shortfall when its remaining work exceeds it. The backend SDKs do this on their own, extending in the background once the lease's unspent balance falls below a configured low-water mark.

1. The second agent extends by 160, the shortfall between the 300 it needs and the 140 available. Only 100 credits are drawable, because the original acquire already took 400 of the company's 500. The lease grows to a `granted_amount` of 500, and the balance reports 0 remaining, 240 reserved, and 240 settled.
2. Still needing 300 against the 240 the lease now has, the second agent extends again by 60. Nothing is drawable, so the lease is returned unchanged at 500. This is not an error, and no exception is raised.
3. The second agent commits the 240 the lease covers and stops. The balance reports 0 settled, and release returns nothing.

Compare `granted_amount` before and after an extend to determine whether it added anything, because an extend that grew the lease by nothing is indistinguishable in shape from one that succeeded. Treat an unchanged `granted_amount` as a signal to stop rather than to proceed.

Without a lease in either case, both agents would read 500 remaining and each would authorize up to 400 credits of work, exposing the balance to 800 credits of consumption against the 500 it holds. In the second case they would have consumed 560 and overdrawn it.

### One inference call

A company has 100 credits. A worker is about to run an inference call that costs at most 25 credits, and it takes a reservation for that amount.

1. The reservation debits 25. The balance reports 75 remaining, 25 reserved, and 100 settled.
2. The call runs and uses 18 credits. The worker sends a track event carrying the `reservation_id` and a quantity of 18.
3. The reservation settles. The unspent 7 return to the grants they came from, and the balance reports 82 remaining, 0 reserved, and 82 settled.

If the balance had been 20 rather than 100, the reservation would have failed with a `402` and consumed nothing. A reservation is funded in full or not at all.

If a second worker takes its own reservation for 25 while the first is open, it gets a separate reservation. The balance reports 50 remaining and 50 reserved. Each settles independently.

### A multi-step workflow

A three-step document workflow estimated at 900 credits acquires a lease for that amount up front, then commits after each step as that step's actual cost becomes known:

| Step      | Committed by this step | Total committed | Lease unspent |
| --------- | ---------------------- | --------------- | ------------- |
| Extract   | 220                    | 220             | 680           |
| Summarize | 410                    | 630             | 270           |
| Index     | 95                     | 725             | 175           |

Release then returns the unspent 175 to the balance.

Reserve once for the whole flow rather than once per step. You then authorize the full cost while abandoning the work is still cheap, instead of hitting a depleted balance partway through. See [Incremental commits across a multi-step workflow](#incremental-commits-across-a-multi-step-workflow) for what to account for.

## Lifecycle

A lease supports four operations.

| Operation | Effect                                                                                                                          |
| --------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Acquire   | Debits an amount from the company's balance and returns a lease                                                                 |
| Extend    | Increases an open lease, drawing the additional amount from the balance                                                         |
| Commit    | Records actual usage against the lease, without debiting the balance again. May be called more than once against the same lease |
| Release   | Returns the difference between reserved and committed amounts to the balance                                                    |

The pattern corresponds to an authorization and capture on a payment card. Acquire authorizes an upper bound, extend raises that upper bound while the lease is open, commit captures the amount actually used, and release returns the remainder.

A reservation supports reserve, settle, and release. It cannot be extended, and it settles on the first track event that carries its id. See [Reservations](#reservations).

## Balance values while credits are reserved

While a lease or reservation is open, a company's credit balance is reported as three values.

| Value     | Definition                                                     | Use                                        |
| --------- | -------------------------------------------------------------- | ------------------------------------------ |
| Remaining | Drawable amount, excluding reserved credits                    | Deciding whether to authorize further work |
| Reserved  | The unspent portion of open leases and reservations            | Reporting work in flight                   |
| Settled   | Remaining plus reserved, the balance net of actual consumption | Customer-facing balance display            |

Use settled for customer-facing balances. It changes only when work completes, not when credits are reserved or released, so it does not fluctuate while jobs are running. Use remaining for enforcement decisions; it decreases as soon as credits are reserved.

## Leases

A lease lets the SDK gate many operations against credits it already has, instead of asking the server before each one. The sections below describe how a lease behaves on the server. See [Using an SDK](#using-an-sdk) for how the SDK draws on it locally, and [Reservations](#reservations) for the per-operation reserve.

### One lease per company and credit type

Schematic maintains at most one active lease for a given company and credit type. A second acquire against the same company and credit type does not create a second lease; it returns the existing one, and both callers commit their usage against it.

Acquire calls for the same company and credit type are serialized, so two callers arriving simultaneously receive the same lease rather than two leases backed by the same credits. This is the mechanism behind the concurrency guarantee: concurrent workers draw against a single lease that was debited from the balance once, rather than against separate leases that may overlap.

The guarantee is scoped to a single company and credit type. Different credit types, and different companies, lease independently of one another.

### Release once the work is done, not once per operation

Release closes the lease for everyone drawing against it. Acquire, commit, and release as a unit reads naturally when you write it for one operation, and it comes apart the moment a second operation for the same company and credit type overlaps: whichever operation finishes first closes the shared lease and returns the portion covering work that is still running.

The operation still in flight sees three things.

* Its commit falls through to the grants, because a commit against a released lease debits the balance directly. Usage is still billed exactly once.
* Its release returns a 409, `credit lease has already been released`.
* It runs unprotected from the moment the lease closed, so the balance it was gating against is drawable again by anything else.

Scope the lease to the batch of work instead. Acquire once with an `expires_at` covering the whole batch and a `requested_amount` sized for all of it, commit per operation, and release when nothing is left in flight. An open lease costs the reserved credits and nothing else, and [expiry](#expiry) returns them within about a minute if the process dies before it releases.

Do not use a lease to reserve credits for a single operation. Once a second operation for the same company and credit type overlaps the first, the second acquire returns the existing lease rather than adding to it, so the second operation is not covered, and the first release closes the lease while the second operation is still running. Use a [reservation](#reservations) instead. Each reservation is independent, and several can be open for the same company and credit type at once.

### Committing more than a lease covers

Committing more than a lease covers is not blocked. The committed amount is allowed to exceed `granted_amount`, because capping it in the API would silently discard real usage. The caller is the enforcement point.

Suppose the second agent above commits all 300 rather than stopping at the 240 the lease covers, bringing the committed total to 560 against a `granted_amount` of 500.

* The commit is recorded in full. Usage is never dropped.
* The grant was debited 500, the reserved amount, at acquire and extend. The excess 60 is not deducted from any grant, because a commit against a lease draws down the lease rather than the balance.
* Reserved reports 0 rather than a negative value.
* Release returns nothing, since there is no unspent portion to return.

The consequence is that the reported balance overstates the company's position by the overdrawn amount. Settled reads 0 while actual consumption was 560 against a balance of 500. Gate on the lease's unspent portion so this does not arise, and extend rather than over-commit when a step's real cost exceeds the estimate.

### Incremental commits across a multi-step workflow

A lease does not have to be committed in one operation. A long-running workflow can reserve enough credits to cover the whole flow up front, then commit after each step as that step's actual cost becomes known. Each commit reduces the lease's unspent portion, so the reserved value tracks progress through the workflow. The [multi-step workflow example](#a-multi-step-workflow) above shows the numbers.

Four things to account for when a lease spans a workflow rather than a single call.

**Set `expires_at` to cover the whole workflow.** The default is five minutes, which is shorter than most multi-step flows. Set it explicitly, or extend the lease as the work proceeds if the duration is not predictable in advance.

**A commit against a released lease debits the grant directly.** If the lease has already been released, whether explicitly or by the expiry sweep, a track event carrying its `lease_id` is not applied to the lease. It falls through and debits the company's grants instead, so the usage is billed once rather than dropped or debited twice. The remaining steps are no longer covered by a lease, and the lease's unspent portion has already been returned to the balance. The same fall-through applies to a `lease_id` belonging to a different company or credit type.

**Use a distinct idempotency key per step.** Each step is a separate commit, so a retry without its own key is recorded as an additional draw against the lease.

**A step can commit more than the lease covers.** Extend the lease when a step's real cost exceeds what the lease has left, rather than relying on the lease to stop it. See [Committing more than a lease covers](#committing-more-than-a-lease-covers) for what happens if a step commits past it.

### Expiry

Every lease has an expiry. If `expires_at` is not supplied, it defaults to five minutes. Set it explicitly for longer-running work, or extend the lease while the work is in progress.

A lease that reaches its expiry without being released is released automatically, and its unspent portion returns to the balance. A sweep runs once per minute, so credits reserved by a process that terminated between acquire and release are returned within approximately a minute rather than at the end of the billing period. Expiry does not forfeit credits.

Released credits return to the specific grants they were drawn from and retain each grant's original expiry date. A lease drawn partly from a grant expiring this month and partly from one expiring next year returns to both, so [consumption order](/billing/credit-burndown#consumption-order) and [grant expiration](/billing/credit-burndown#credit-lifecycle) behave as they would have without the lease.

### Partial funding

An acquire for more than the balance can cover does not fail. It returns a lease for the amount the balance could back, reported as `granted_amount` on the response. An acquire fails only when no credits are drawable at all.

Read `granted_amount` rather than assuming the requested amount was granted. A caller that requested 1,000 and received 340 must size its work to 340.

## Reservations

A reservation reserves credits for one operation. Taking it debits the full amount from the company's grants, and it closes when you report what the operation actually used.

Take one with `POST /billing/credits/reservations`, passing the company, the credit type, and the amount.

The debit happens under a row lock, so when two operations race for the last of a balance, exactly one succeeds. A balance that cannot cover the full amount returns a `402` and consumes nothing. Reservations are never partially funded, unlike leases; see [Partial funding](#partial-funding). Send an `idempotency_key` to make retries safe. The same key returns the original reservation, and the same key with a different company, credit type, or amount returns a `409`.

Many reservations can be open at once for one company and credit type, alongside a lease if one exists. The company's reserved balance is the sum of every open lease and reservation.

### Settling a reservation

Settle a reservation by sending a normal track event with `reservation_id` set to the reservation's id. The first track carrying the id closes the reservation.

* The tracked usage is recorded against the reservation.
* The unspent remainder returns to the grants it was drawn from.
* Usage beyond the reserved amount is billed as overage, which can push the balance negative.

Send exactly one settling track. A second track carrying the same `reservation_id` falls through to a normal grant debit, because the reservation is already closed, so use the events API's [idempotency key](/playbooks/metering) when you retry. A track body accepts `lease_id` and `reservation_id`, and `lease_id` takes precedence when both are present. Send one or the other.

The events pipeline settles asynchronously. Do not expect the balance or the reservation's `settled_amount` to reflect a track the moment you send it.

### Releasing and expiry

`PUT /billing/credits/reservations/{id}/release` cancels a reservation and returns the whole reserved amount. Use it only when the operation did not happen. It is idempotent, so releasing a reservation that is already released or settled returns the row unchanged.

`expires_at` defaults to one minute from now and may be at most one hour out. A sweep runs every ten seconds and refunds any reservation that reaches its expiry unsettled. Size `expires_at` above the operation's expected duration plus a few seconds of margin. A track that arrives after expiry is still billed, as a normal grant debit.

### Checking and reserving in one call

`POST /flags/{key}/check-and-reserve` evaluates a flag exactly as a plain check does and, when the flag allows and the matched entitlement is credit metered, takes the reservation in the same call. One round trip decides whether the operation may run and reserves what it will cost.

The response is the normal check response with a `reservation` object added, carrying the reservation's `id`, its `credits_reserved`, and the `event_subtype` your settling track should send.

When the balance cannot fund the reservation, the response is a `200` with `value` false and the reason `Insufficient credits`, rather than the `402` the reservations endpoint returns. A flag that denies, a feature that is not credit metered, and a credit whose ledger lives on an external billing provider all return the plain check result with no `reservation`, and you track usage as usual. The check is logged like any other.

The endpoint requires a secret API key. Publishable keys cannot draw down a balance.

## Using an SDK

The backend SDKs support both leases and reservations through their check and track-with-reservation methods, in two modes. Frontend SDKs do not take leases or reservations, because a publishable key cannot draw down a balance.

**Client mode** maintains one lease per company and credit type and issues per-request reservations against that lease locally, so an individual check does not require an API round trip, and reservations are gated across processes through a shared cache. It requires DataStream. Use it for gating high volumes of inference calls or agent runs.

**Server mode** makes one check-and-reserve call per check and settles it with a track event. It requires neither DataStream nor Redis.

Each SDK exposes a credit lease mode setting. The default is `auto`, which picks client mode when DataStream is enabled and server mode otherwise. Each [SDK reference](/developer_resources/sdks/overview) covers configuration, failure behavior, and pre-warming.

Without an SDK, call the endpoints in [Using the API](#using-the-api) directly. For a single operation, use [check and reserve](#checking-and-reserving-in-one-call), or take a reservation and settle it with a track event.

## Technical details

In client mode the SDK keeps one lease per company and credit type and issues per-request reservations out of it. The tunable settings are the lease size, the lease and reservation lifetimes, and the top-up threshold. The names below follow the SDK's naming conventions, so they vary in case between languages.

| Setting                 | Behavior                                                                                                                       | Default    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `defaultLeaseSize`      | Credits requested per acquire or extend                                                                                        | 10,000     |
| `defaultLeaseDuration`  | How long the lease stays open before it is swept                                                                               | 5 minutes  |
| `lowWaterMark`          | The lease extends in the background once its unspent portion falls below this fraction, so a check does not wait on an acquire | 0.25       |
| `defaultReservationTTL` | How long an unsettled reservation keeps its slice of the lease                                                                 | 60 seconds |
| `sweepIntervalMs`       | How often expired reservations are returned to the lease                                                                       | 1 second   |

An `overrides` map sets any of these per credit type, keyed by credit type ID.

Two behaviors are worth understanding before changing the defaults.

**The reservation TTL should exceed the longest gap between a check and its settle.** A reservation that expires first returns its credits to the lease. The settle still bills the usage, and the track event's idempotency key means a late or retried settle cannot double-bill, but the local lease balance reads high until the lease rolls over.

**A check that cannot be gated fails closed.** When the API is unreachable, the shared cache is down, or the lease is exhausted, the check denies. Failing open is set per check, for callers where a denial is worse than letting the work through, and it bypasses only the credit gate. Plan targeting, overrides, and every other rule on the flag still run.

Lease and reservation state lives in the same shared cache as the SDK's flag and company data. Across several processes that cache has to be shared, or each process gates against a lease balance only it can see.

## Using the API

### Lease endpoints

Acquire a lease with [Acquire credit lease](/api-reference/billing-credits/acquire-credit-lease):

```bash
curl -X POST https://api.schematichq.com/billing/credits/lease \
  -H "X-Schematic-Api-Key: $SCHEMATIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "company_id": "comp_EXAMPLE0123456789",
    "credit_type_id": "bilcr_EXAMPLE0123456789",
    "requested_amount": 400
  }'
```

The response contains the lease's `id`, its `granted_amount`, and the `tracked_amount` committed against it so far. Size each operation against the difference between the two, which is what the lease has left. Acquire, extend, and release all return the same shape, so an extend tells you the new headroom without a second call.

Commit usage against the lease by sending a track event with `lease_id` set:

```bash
curl -X POST https://api.schematichq.com/events \
  -H "X-Schematic-Api-Key: $SCHEMATIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "track",
    "body": {
      "event": "inference_tokens",
      "company": { "id": "comp_EXAMPLE0123456789" },
      "lease_id": "crlse_EXAMPLE0123456789",
      "quantity": 260
    }
  }'
```

Include an [idempotency key](/playbooks/metering) on the track event. Without one, a retried commit is recorded as an additional draw against the lease.

Release the lease with [Release credit lease](/api-reference/billing-credits/release-credit-lease), once every operation drawing against it has committed. A release while another operation is still running closes the lease under it; see [Release once the work is done, not once per operation](#release-once-the-work-is-done-not-once-per-operation).

```bash
curl -X PUT https://api.schematichq.com/billing/credits/lease/crlse_EXAMPLE0123456789/release \
  -H "X-Schematic-Api-Key: $SCHEMATIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

To increase an open lease, [Extend credit lease](/api-reference/billing-credits/extend-credit-lease) accepts an `additional_amount` and an optional new `expires_at`.

### Reservation endpoints

Reserve credits for a single operation with [Reserve credits](/api-reference/billing-credits/reserve-credits):

```bash
curl -X POST https://api.schematichq.com/billing/credits/reservations \
  -H "X-Schematic-Api-Key: $SCHEMATIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "company_id": "comp_EXAMPLE0123456789",
    "credit_type_id": "bilcr_EXAMPLE0123456789",
    "amount": 25,
    "expires_at": "2026-01-01T00:00:30Z",
    "idempotency_key": "job_EXAMPLE0123456789"
  }'
```

`amount` must be positive. The full amount is reserved, or the request fails. `expires_at` is optional, defaults to one minute out, and may be at most one hour out. The response carries the reservation's `id`, its `reserved_amount`, the `settled_amount` recorded so far, and `released_at`, which is null while the reservation is open.

Settle the reservation with a track event carrying `reservation_id`:

```bash
curl -X POST https://api.schematichq.com/events \
  -H "X-Schematic-Api-Key: $SCHEMATIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "track",
    "body": {
      "event": "inference_tokens",
      "company": { "id": "comp_EXAMPLE0123456789" },
      "reservation_id": "crlse_EXAMPLE0123456789",
      "quantity": 18
    }
  }'
```

Cancel a reservation whose operation never ran with [Release credit reservation](/api-reference/billing-credits/release-credit-reservation), which takes no body:

```bash
curl -X PUT https://api.schematichq.com/billing/credits/reservations/crlse_EXAMPLE0123456789/release \
  -H "X-Schematic-Api-Key: $SCHEMATIC_API_KEY"
```

To check a flag and reserve credits in one call, [Check and reserve flag](/api-reference/features/check-and-reserve-flag) accepts the body a check accepts plus `quantity`, `expires_at`, and `preflight`. The reserved amount is the entitlement's consumption rate times the units the request states: `quantity` when it is given, otherwise `preflight.event_usage.quantity` when its `event_subtype` is the entitlement's, otherwise `preflight.usage`, otherwise 1. `preflight.credit_cost` takes precedence over all of those: when it names the entitlement's credit, that cost is reserved as given. Pass an `idempotency_key` to make the call safe to retry: a second request with the same key returns the reservation the first one took instead of reserving again, even when the first hold has since lowered the balance.

## Ledger entries

Each operation is recorded in the [credit ledger](/use-cases/credit-ledger). Ledger entries carry a usage reason, and leases and reservations introduce four of them.

| Usage reason          | Recorded when                                                               |
| --------------------- | --------------------------------------------------------------------------- |
| `lease_hold`          | Credits are debited at acquire or extend                                    |
| `lease_release`       | The unspent portion is returned at release, expiry, or sweep                |
| `reservation_hold`    | A reservation is taken, and again when settling it bills overage            |
| `reservation_release` | A reservation's unspent remainder is returned at settle, release, or expiry |
| `track`               | Usage is committed, against a lease or reservation, or otherwise            |

A lease that reserved 400 and consumed 350 produces three sets of entries: the 400 debit, the committed usage, and the 50 return, each with its own timestamp. No netting is applied. Reservation rows are kept after they settle or release, so the ledger still reconciles against them.

## Related

* [Credit Burndown Billing Model](/billing/credit-burndown) - credit types, grants, balances, and expiry.
* [Meter on a real-time credit ledger](/use-cases/credit-ledger) - why enforcement needs a transactional ledger rather than a warehouse snapshot.
* [Spend & Usage Controls](/billing/spend-controls) - the limits and top-up settings a balance sits inside.
* [Creating a metered feature](/playbooks/metering) - sending the usage events that commit against a lease or reservation.