Checks and holds
check() answers one question before an agent starts work: can this customer pay for it? With hold: true, it also reserves the cost, so other work can’t spend the same balance while this work runs.
Describe the work
Pass exactly one of these, and Runledger prices it from the customer’s plan:
A denial is a normal response with allowed: false and a reason:
The response also returns what the work costs (required, which is "0" for no_active_subscription and no_rate_for_event, where there is no plan or rate to price it from) and what was available before this call, so you can show the customer how far short they are and offer a top-up. The affordability block gives the same figures for the balance, pools, allowances and spend limits. When an allowance or a spend limit denies the work, denied_by names which one and when it lifts.
A denial carries one reason. If more than one test would deny the work, which reason you get isn’t specified, so handle every reason in the table.
How far the work fits
Every check() response carries an affordability block, allowed or denied, with or without hold: true. It says what the work would do to the balance, the pools, every allowance that applies and the spend limits, so your product can show a customer “this uses about 40 of your 1,260 credits” before the work starts. The block is always there, and so is every field in it: a value that doesn’t apply is null, and a list with nothing in it is []. POST /v1/events/preview prices an event and doesn’t return it.
A customer with no pools, allowances or limits gets empty lists. When a plan has them, each is listed. Here a spend limit with action: warn would be taken 15 past its room: the limit lists its shortfall of "15", the work is allowed, and would_exceed is false, because a warn-only limit doesn’t block anything.
Past an allowance: overage, or a stop
Every allowance that applies is listed, and on_exhausted says what happens to the part of the work that goes past it. The same numbers read two ways. A plan gives 100 credits a month, 90 are used, and the job needs 30, so the window has remaining_before of "10", remaining_after of "-20" and a shortfall of "20".
When the allowance bills overage, the 20 past it are billed as overage. The work is allowed, and would_exceed is false. This postpaid plan includes 100 credits a month of the credits event and bills overage at 5 minor units a credit, so required is 20 × 5 = 100:
When the allowance stops at its cap, the same 20 are over a limit. The work is denied, and would_exceed is true. This prepaid plan caps use at 100 credits a month, and the work is 30 credits, so the figures are the same. The balance has plenty, so it isn’t short:
An allowance with on_exhausted: draw_next_pool lists the same kind of shortfall, the amount that moves on to the next pool, and leaves would_exceed false unless the balance can’t cover it.
What the figures mean
Units. Balance and pool amounts are decimal strings in the customer’s balance unit: credits of the credit_type on a prepaid plan, and minor units of the plan’s currency on a postpaid plan. Allowance amounts are credits, or units of the event for an included quantity. Spend limit amounts are minor units of that limit’s own currency. Currencies are never converted or added together.
Before and after. A _before figure is read before this call’s own hold, and counts every other active hold. It is the same figure as available on the response. An _after figure is the _before figure minus what the work uses. Without hold: true nothing is reserved, and _after is what would be left if the work ran. With hold: true on an allowed check the hold is for required, so _after is what the next caller sees. A denied check places no hold, and its _after figures are worked out the same way.
A denial still carries reason and denied_by, and affordability shows the figures behind them. For no_active_subscription and no_rate_for_event there is no balance or price to measure against, so required is "0", the balance figures are null, pools, allowances and spend_limits are [], and would_exceed is false:
Without hold: true the block is a point-in-time read, not a reservation. Another caller can use the balance, an allowance’s room or a spend limit’s room right after you read it, so a figure that said the work fits can be out of date by the time the work starts. Work that must not lose its place needs hold: true. Checks against one balance are serialized, so a hold can’t double-spend it. That guarantee is stated for balances, and the block doesn’t promise more for caps or spend limits.
Holds
A hold counts against the available balance until one of these happens:
track()captures it. Pass the hold’s ID asholdon the event. The event’s price is captured and the rest released, unless you setrelease_remainder: falseto capture again later, for example once per step of a long run.- You release it with
POST /v1/holds/{id}/release, when the work is canceled or fails. - It expires. The default lifetime is 15 minutes, set by
hold_expires_in(any ISO 8601 duration, up to 30 days). Expired holds release automatically and sendhold.expired.
Long-running agents can extend a hold with POST /v1/holds/{id}/extend, passing a new expires_in and, if the work grew, a new total amount. expires_in on an extend is in whole days only: P1D to P30D, counted from now, such as P3D. A duration in hours or minutes, such as PT2H, returns 400 with parameter_invalid. That differs from hold_expires_in on check(), which can be minutes (the default is 15 minutes), so an extend always moves the expiry at least a day out. If you only need a little more time, place the hold with a longer hold_expires_in to begin with. A larger total must fit the available balance or the request fails with insufficient_balance, and the hold keeps its current amount.
When the captured event is an outcome, the reservation moves to the pending outcome charge and stays held until the charge confirms or is reversed.
Margin
check() can also compare a price with the cost you expect and deny work that would sell below a margin floor. A guardrail’s action decides what you see: block denies the work with the reason margin_below_floor, and warn allows it and adds the warning margin_below_guardrail. track() only ever warns, with margin_below_guardrail. They are one guardrail seen two ways: margin_below_floor is a reason for a denial, and margin_below_guardrail is a code on a warning. It takes optional properties and estimated_usage. See Pricing from cost.
Two agents, one balance
This is the failure holds exist to prevent. A customer has 100 credits. Two agents start at the same moment, and each run will cost 60.
Checks against one balance are serialized, so two concurrent check() calls can never both reserve the same credits. The sandbox scenarios include this race as a script you can run.
Holds and changing balances
A hold reserves an amount of a credit type, not particular grants. Credits are drawn from grants only at capture, in the published burn-down order. So a hold placed before the monthly reset survives it, and a hold backed by credits that expire before capture draws from whatever is left. If the balance can no longer cover an active hold, for example after a grant is voided, it shows as overdrawn and the next check is denied.
Work that already happened
track() never fails for lack of balance, because the work is done. If an event costs more than its hold, or there was no hold, the difference is drawn from the available balance. A prepaid balance may go below zero by the plan’s overdraft_limit (default "0"); past it, the balance is overdrawn, track() warns balance_overdrawn, balance.depleted fires, and checks are denied until the customer tops up.
Postpaid customers
On a postpaid plan, checks and holds work against the credit limit: available = credit limit - unbilled charges - held. Without a credit limit, checks are allowed unless the subscription is past_due. Holds are still worth placing, because they show what is in flight.
Limits and windows
A balance says what a customer has. Two more tests say how fast it may be used, and by whom:
- Usage windows refill a plan’s credits on a schedule and cap use per week, per five-hour session or per hour. A cap that is full denies with
allowance_exhausted. - Spend limits cap the money value of what a customer, team, member or agent consumes in a period. A limit that is full denies with
spend_limit_reached.
Active holds count against both, so two agents can’t reserve the same room. track() never blocks. Work that already happened is recorded, and a window or limit it takes past its amount adds the warning allowance_exhausted or spend_limit_exceeded. Holds, events and costs record agent_id, member_id and team_id, so you can see and cap spend by any of them. See Members, teams and agents.
Alerts at thresholds you choose are webhooks: usage.threshold_reached, spend_limit.threshold_reached and balance.low, besides balance.depleted. See Auto top-up and alerts.
Next steps
- Prepaid and postpaid billing explains the balance a check reads.
- Going live covers timeouts and failing open.
- API reference: check lists every field.