> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runledger.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runledger.co/_mcp/server.

# Run a simulation

POST https://api.runledger.co/v1/simulations
Content-Type: application/json

Replays recorded events and costs from a past range against a candidate: a configuration document with cost models, rate cards, price rules or guardrails, and cost adjustments such as "this model costs 20% more". It returns the margin the range would have had, next to the margin it had. Nothing is written to customer balances, invoices or live configuration. Costs priced from the catalog are priced again from their usage lines, and costs you reported yourself with `cost` are kept as reported. Outcome charges and their reversals stay as recorded, so the margin still counts reversals. A simulation covers at most 31 days. It may return as `pending`; fetch it, or wait for `simulation.completed`. Needs `config:read` and `events:read`, and works with sandbox and live keys.

Reference: https://docs.runledger.co/api-reference/simulations/create

## Authentication

- `Authorization` header (bearer token, required) — A secret key (`rl_sk_test_...`, `rl_sk_live_...`) or a restricted key with limited scopes (`rl_rk_test_...`, `rl_rk_live_...`). A restricted key can carry `agent: true` and an `expires_at`, and is what a coding agent should hold. Keys end in a checksum so secret scanners can recognize them. People create, roll and revoke keys in the dashboard; `GET /v1/api_keys` lists them, masked.

## Request

### Body (application/json)

This endpoint expects a CreateSimulationRequest.

- `from` (datetime, required) — Start of the range, inclusive.
- `to` (datetime, required) — End of the range, exclusive. At most 31 days after `from`.
- `currency` (string, required) — Report in this currency. Other currencies are left out, as in the margin report.
- `document` (SimulationDocument, optional) — The candidate. Sections have the shape of the matching create requests, and replace the current object with the same key for the simulation only.
- `cost_adjustments` (list of CostAdjustment, optional)
- `customer_id` (string, optional) — Only this customer (`cus_...`).
- `plan` (string, optional) — Only customers on this plan (ID or key).
- `group_by` (enum, optional, default: model)
  - Allowed values: `customer`, `model`, `definition`
- `reason` (string, optional)

## Response

### 200

The simulation.

- `id` (string, required)
- `object` ("simulation", required)
- `status` (enum, required)
  - Allowed values: `pending`, `completed`, `failed`
- `from` (datetime, required)
- `to` (datetime, required)
- `currency` (string, required)
- `livemode` (boolean, required)
- `created_at` (datetime, required)
- `group_by` (enum, optional)
  - Allowed values: `customer`, `model`, `definition`
- `document` (SimulationDocument, optional) — The candidate. Sections have the shape of the matching create requests, and replace the current object with the same key for the simulation only.
- `cost_adjustments` (list of CostAdjustment, optional)
- `results` (SimulationResults, optional) — Present when `status` is `completed`.
- `error` (ErrorResponse, optional) — Present when `status` is `failed`.
- `actor` (Actor, optional) — Who made a change. Runledger fills it in from the credential, so a client can't claim to be someone else: an API key always acts as itself, the dashboard calls the API as the signed-in person, and the MCP server passes the client's name and the person it acts for. A request that sends a `Runledger-Actor` header is refused with `400 parameter_unknown`.
- `completed_at` (datetime, optional)

## Errors

### 400 Bad Request Error

The request was malformed or a parameter was invalid. A configuration that fails validation, such as two equally specific overrides with overlapping dates, returns `config_invalid`.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 401 Unauthorized Error

The API key is missing, invalid or revoked.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 403 Forbidden Error

The key can't do this. A restricted or agent key lacks the scope (`insufficient_scope`, with `required_scope`), a live key called a sandbox-only endpoint, an agent tried a live config write that needs a proposal (`proposal_required`), or something only a signed-in person can do.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 422 Unprocessable Entity Error

The idempotency key was already used with a different request body.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 429 Too Many Requests Error

Too many requests. Wait for the number of seconds in `Retry-After`.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 500 Internal Server Error

Something went wrong on Runledger's side. Safe to retry with the same idempotency key.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

## Types

### SimulationDocument

The candidate. Sections have the shape of the matching create requests, and replace the current object with the same key for the simulation only.

- `cost_models` (list of CreateCostModelRequest, optional)
- `price_rules` (list of CreatePriceRuleRequest, optional)
- `margin_guardrails` (list of CreateMarginGuardrailRequest, optional)

### CostAdjustment

A what-if on what a cost model charges, applied to every line priced from it. Send `multiplier`, `catalog_price`, or both: the catalog price is applied first, then the multiplier.

- `cost_model` (string, required) — The cost model (ID or key).
- `unit` (string, optional) — Limit the adjustment to one unit. Leave it out for every unit.
- `multiplier` (string, optional) — A decimal string above zero. `"1.2"` is 20% more.
- `catalog_price` (string, optional) — A catalog cost model only. Price its lines from this catalog price version (`catp_...`), usually one that is scheduled, instead of the version in force at each line's time. Your overrides still apply.

### SimulationResults

- `events_considered` (integer, required)
- `costs_considered` (integer, required)
- `events_unpriced` (integer, required) — Events the candidate could not price, for example because no rate matched. They are left out of the candidate's revenue.
- `baseline` (SimulationTotals, required) — What the range actually recorded.
- `candidate` (SimulationTotals, required) — What it would have recorded under the candidate.
- `by_group` (list of SimulationGroupRow, optional)
- `guardrails` (list of SimulationResultsGuardrailsItems, optional) — How often each guardrail in the candidate, or in force, would have warned or blocked.
- `warnings` (list of ConfigIssue, optional)

### ErrorResponse

An RFC 9457 problem details object with Runledger's extension members.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### Actor

Who made a change. Runledger fills it in from the credential, so a client can't claim to be someone else: an API key always acts as itself, the dashboard calls the API as the signed-in person, and the MCP server passes the client's name and the person it acts for. A request that sends a `Runledger-Actor` header is refused with `400 parameter_unknown`.

- `type` (enum, required) — `user`: a signed-in person, including a person at Runledger support acting in your account, whose `name` reads `Runledger support`. `agent`: an agent connected through an OAuth session or an agent key. `key`: any other API key. `rule`: a reversal rule. `system`: Runledger itself, such as a clock.
  - Allowed values: `user`, `agent`, `key`, `rule`, `system`
- `id` (string, optional) — The user, key or agent session ID. Left out for `rule` and `system`, except a change a customer makes on the customer view, which carries the customer session's ID.
- `name` (string, optional) — A display name, such as a person's name, a key's name or the MCP client.
- `on_behalf_of` (string, optional) — The person an agent acts for, when known.
- `client` (string, optional) — The software that made the call, such as an MCP client or SDK.

### ConfigIssue

A problem or warning about a part of a configuration document.

- `code` (string, required) — A stable code. New codes may be added.
- `message` (string, required)
- `path` (string, optional) — Where in the document, as a dotted path.

### ErrorResponseBlockedByItems

- `object` (enum, required) — The kind of object in the way: a routing rule, or a customer with a `payment_provider` override.
  - Allowed values: `routing_rule`, `customer`
- `id` (string, required) — The object's ID, `rr_...` for a routing rule and `cus_...` for a customer.

### CreateCostModelRequest

- `key` (string, required) — A key you choose. Requests accept the key or the ID.
- `name` (string, required)
- `source` (enum, optional) — Where the rates come from. Default `catalog` when `catalog_model` is sent, and `custom` otherwise.
  - Allowed values: `catalog`, `custom`
- `catalog_model` (string, optional) — Takes the provider, model, kind, units, dimensions and rates from this catalog model (`catm_...`), and follows its price versions from each `effective_from`. With it, leave out `provider`, `model`, `kind`, `units`, `dimensions` and `rate_card`. Use `overrides` to change a rate.
- `overrides` (list of CostOverride, optional) — Your own rates for a catalog cost model. Not for a custom cost model, whose rate cards are already yours.
- `provider` (string, optional) — A custom cost model. The provider as `trace()` names it.
- `model` (string, optional) — A custom cost model. The model ID as the provider names it, as `trace()` sends it.
- `kind` (enum, optional) — What the model produces. Open: new values may be added.
  - Allowed values: `text`, `image`, `video`, `audio`, `speech`, `embedding`, `compute`, `agent_session`, `other`
- `units` (list of CostUnitDeclaration, optional)
- `dimensions` (list of CostDimension, optional)
- `rate_card` (RateCardInput, optional) — A custom cost model. The first rates. Without `effective_from` the card is active from the moment the model is created, and a given `effective_from` can't be in the past. In a configuration document, a `rate_card` adds a new version only when its rates differ from the latest, so applying the document twice changes nothing the second time.
- `metadata` (map from string to string, optional) — Up to 50 keys of your own, with keys up to 40 characters and values up to 500 characters.

### CreatePriceRuleRequest

- `name` (string, required)
- `currency` (string, required) — Lowercase ISO 4217 code. Must match the currency of the rate cards the rule reads and of the plans that use it: Runledger doesn't convert between currencies, so a rule that mixes them is a validation error.
- `key` (string, required)
- `method` (enum, required) — `cost_plus`: the cost of the usage plus a markup or a target margin. `table`: a price looked up by dimension values. `formula`: an expression.
  - Allowed values: `cost_plus`, `table`, `formula`
- `description` (string, optional, nullable) — The label on invoices. On a change, `null` clears it.
- `basis` (enum, optional, default: catalog) — What `cost_plus` and `formula` rules take the cost from. `catalog`: the cost models and the event's quantity and properties, the same at `check()` and `track()`. `run_cost`: the costs `trace()` recorded under the event's `run_id`, for pass-through pricing after the work. `check()` can't know that cost yet, so it prices from `estimated_usage` or, without it, from the observed average.
  - Allowed values: `catalog`, `run_cost`
- `inputs` (list of PriceRuleInput, optional) — `cost_plus` and `formula` rules on the `catalog` basis.
- `markup_percent` (string, optional) — `cost_plus`. The price is the cost times one plus this, as a decimal string: `"0.4"` is cost plus 40%. Send this or `target_margin_percent`.
- `target_margin_percent` (string, optional) — `cost_plus`. The price is the cost over one minus this, so the margin is this share of the price, as a decimal from 0 to 1. `"0.3"` is a 30% margin.
- `rows` (list of PriceTableRow, optional) — `table` rules.
- `formula` (string, optional) — `formula` rules. An expression over `cost`, `quantity` and numeric event properties written `properties.<name>`, with `+`, `-`, `*`, `/`, brackets, `min()`, `max()` and `ceil()`. It returns minor units of `currency`.
- `floor_decimal` (string, optional, nullable) — The lowest price for one event, in minor units. The price is raised to it. On a change, `null` clears it.
- `cap_decimal` (string, optional, nullable) — The highest price for one event, in minor units. The price is lowered to it. On a change, `null` clears it.
- `credit_value_decimal` (string, optional) — Prepaid plans. Minor units of `currency` that one credit is worth, used to turn the price into credits. Defaults to the plan's top-up price per credit.
- `on_cost_change` (enum, optional, default: hold) — What the rule does when a new rate card changes the cost it prices from, whether you added it or Runledger published a new catalog price. `hold`: it keeps pricing from the versions in force when you last saved it, so the margin moves and a person decides whether to change the price from a later date. `follow`: the price follows the new card from its `effective_from`.
  - Allowed values: `hold`, `follow`
- `effective_from` (datetime, optional) — When this version starts to price events. Default now.

### CreateMarginGuardrailRequest

- `name` (string, required)
- `min_margin_percent` (string, required) — The lowest margin allowed, as a decimal from 0 to 1 of the price. `"0.25"` is 25%. Negative values are not accepted.
- `key` (string, required)
- `applies_to` (GuardrailScope, optional) — Every condition you send must match. A guardrail with none covers all work.
- `basis` (enum, optional, default: expected) — `expected`: the margin the work should have, from its price and its expected cost at `check()` time. `realized`: confirmed revenue minus cost over the trailing `window` for the work the guardrail covers, as the margin report counts it, so reversals lower it.
  - Allowed values: `expected`, `realized`
- `action` (enum, optional, default: warn) — `warn` adds warnings and a webhook. `block` also denies in `check()`.
  - Allowed values: `warn`, `block`
- `window` (string, optional) — An ISO 8601 duration. For `realized`, the trailing range the margin is read over. For `expected`, the range the observed average cost is read over when `check()` has no `estimated_usage`. Default P7D.
- `enabled` (boolean, optional, default: true)

### SimulationTotals

Decimal minor units of the simulation's currency, as on a run. Margin is confirmed revenue minus cost.

- `revenue_confirmed` (string, required)
- `revenue_pending` (string, required)
- `revenue_reversed` (string, required)
- `cost` (string, required)
- `margin` (string, required)
- `margin_percent` (string, optional)

### SimulationGroupRow

- `group` (string, required) — A customer ID, a provider and model as `provider/model`, or an outcome definition key.
- `baseline` (SimulationTotals, required) — Decimal minor units of the simulation's currency, as on a run. Margin is confirmed revenue minus cost.
- `candidate` (SimulationTotals, required) — Decimal minor units of the simulation's currency, as on a run. Margin is confirmed revenue minus cost.

### SimulationResultsGuardrailsItems

- `key` (string, required)
- `would_warn` (integer, required)
- `would_block` (integer, required)

### CostOverride

Your own rate, layered on a catalog cost model. For each usage line, an override that is in force at the line's timestamp and matches its unit and dimensions beats the catalog price, and the most specific match wins: one that names a unit and dimensions beats one that names only a unit, and that beats one that names neither. Two equally specific overrides whose dates overlap are a validation error: the write is refused with `400` and `config_invalid`, and nothing is saved. Send exactly one of `discount_percent` and `unit_amount_decimal`.

- `key` (string, required) — A key you choose, unique within the cost model. Cost lines name it when it priced them.
- `unit` (string, optional) — Limit the override to one unit. Leave it out for every unit of the model.
- `dimensions` (map from string to string, optional) — Limit the override to lines with these dimension values. Leave a dimension out to match any value.
- `discount_percent` (string, optional) — A decimal from 0 to 1 taken off the catalog rate of every matching line, tiers included. `"0.2"` is 20% below the catalog price.
- `unit_amount_decimal` (string, optional) — Replaces the catalog rate with this one, in minor units for `per` units. Needs `unit`. A unit the catalog model doesn't have is added, which is how you extend a catalog model.
- `per` (string, optional, default: 1) — How many units `unit_amount_decimal` covers, as a decimal string.
- `effective_from` (datetime, optional) — When the override starts to apply. Leave it out to apply from the moment it is saved.
- `effective_to` (datetime, optional) — When it stops applying, such as the end of a commitment. Leave it out for no end.
- `category` (enum, optional) — A label for reporting. It changes no price. Open: new values may be added.
  - Allowed values: `negotiated_discount`, `committed_spend`, `private_deployment`, `self_hosted`, `other`
- `reason` (string, optional) — Why, in your words. For example the contract it comes from. Recorded in the audit log.

### CostUnitDeclaration

One unit a cost model is metered in.

- `unit` (string, required) — What is counted. Use `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens`, `images`, `video_seconds`, `audio_seconds`, `gpu_seconds`, `session_hours`, `requests` or `searches`, or your own plural, such as `tool_calls`.
- `controlled_by` (enum, required) — Who decides the value. `customer`: your end customer's request sets it, so it is known before the work starts and `check()` can price it. Examples are the resolution, the quality, the seconds of video asked for and the number of reference images. `provider`: the model provider or your own infrastructure sets it while the work runs, so it is known only afterwards and `trace()` reports it. Examples are the input and output tokens and the GPU seconds.
  - Allowed values: `customer`, `provider`
- `description` (string, optional)

### CostDimension

A property that changes the rate for a unit, such as resolution, quality, context bucket or service tier.

- `key` (string, required)
- `controlled_by` (enum, required) — Who decides the value. `customer`: your end customer's request sets it, so it is known before the work starts and `check()` can price it. Examples are the resolution, the quality, the seconds of video asked for and the number of reference images. `provider`: the model provider or your own infrastructure sets it while the work runs, so it is known only afterwards and `trace()` reports it. Examples are the input and output tokens and the GPU seconds.
  - Allowed values: `customer`, `provider`
- `values` (list of string, optional) — The values the rate cards may use. A usage line with another value gets the `rate_not_found` warning.
- `description` (string, optional)

### RateCardInput

- `currency` (string, required) — Lowercase ISO 4217 code the rates are in.
- `rates` (list of CostRate, required)
- `effective_from` (datetime, optional) — When this version starts to price usage. It must be now or later, and after the start of every earlier version of the cost model, including a scheduled one. A new version applies only from this date onward. Recorded usage is never repriced: events and costs dated before it keep the earlier version. Leave it out to save a draft.
- `as_of` (date, optional) — The date you read these rates from the provider, so a person can see how fresh they are.
- `source_reference` (string, optional) — Where the rates came from, such as the provider's pricing page address or a contract reference.

### PriceRuleInput

One cost line a `catalog` rule prices. The event's dimension values are read from its `properties`, so name the cost model's dimensions there.

- `cost_model` (string, required) — The cost model (ID or key).
- `unit` (string, required)
- `quantity_from` (string, optional, default: quantity) — `quantity` takes the event's quantity. `properties.<name>` takes a numeric property of the event.

### PriceTableRow

One row of a `table` rule. Send `unit_amount_decimal` or `tiers`.

- `dimensions` (map from string to string, optional) — The property values this row is for. Leave a property out to match any value. The most specific row wins.
- `unit_amount_decimal` (string, optional) — Minor units of the rule's currency per unit of quantity.
- `tier_mode` (enum, optional, default: graduated)
  - Allowed values: `graduated`, `volume`
- `tiers` (list of RateTier, optional)

### GuardrailScope

Every condition you send must match. A guardrail with none covers all work.

- `event_name` (string, optional)
- `outcome_definition` (string, optional) — An outcome definition (ID or key).
- `plan` (string, optional) — A plan (ID or key).

### CostRate

The price of one unit, for the usage lines whose dimensions match. A rate with fewer dimensions matches more lines, and the most specific matching rate wins. Send `unit_amount_decimal` or `tiers`.

- `unit` (string, required)
- `per` (string, optional, default: 1) — How many units `unit_amount_decimal` covers, as a decimal string. Use `"1000000"` for a price per million tokens.
- `unit_amount_decimal` (string, optional) — Minor units of the rate card's currency for `per` units, as a decimal string.
- `dimensions` (map from string to string, optional) — The dimension values this rate is for. Leave a dimension out to match any value.
- `tier_mode` (enum, optional, default: graduated) — With `tiers`. `graduated`: each band's price applies to the quantity inside that band. `volume`: the band the whole quantity falls in prices all of it, as when a long prompt is priced higher in full.
  - Allowed values: `graduated`, `volume`
- `tiers` (list of RateTier, optional)

### RateTier

One band of a quantity. The last band leaves out `up_to`.

- `unit_amount_decimal` (string, required) — Minor units per unit of the band, as a decimal string.
- `up_to` (string, optional) — The quantity this band runs up to, as a decimal string.

## Examples

**Request**

```json
{
  "from": "2024-01-15T09:30:00Z",
  "to": "2024-01-15T09:30:00Z",
  "currency": "usd"
}
```

**Response**

```json
{
  "id": "sim_5wD2nF8qTj",
  "object": "string",
  "status": "pending",
  "from": "2024-01-15T09:30:00Z",
  "to": "2024-01-15T09:30:00Z",
  "currency": "usd",
  "livemode": true,
  "created_at": "2024-01-15T09:30:00Z",
  "group_by": "customer",
  "document": {
    "cost_models": [
      {
        "key": "vidlab-v2",
        "name": "Vidlab v2 video",
        "source": "catalog",
        "catalog_model": "catm_5xT7qD2mKa",
        "overrides": [
          {
            "key": "textco_negotiated",
            "unit": "input_tokens",
            "dimensions": {
              "resolution": "1080p"
            },
            "discount_percent": "0.2",
            "unit_amount_decimal": "10",
            "per": "1",
            "effective_from": "2024-01-15T09:30:00Z",
            "effective_to": "2024-01-15T09:30:00Z",
            "category": "negotiated_discount",
            "reason": "Negotiated 20% off to 31 December 2026"
          }
        ],
        "provider": "vidlab",
        "model": "vidlab-v2",
        "kind": "text",
        "units": [
          {
            "unit": "video_seconds",
            "controlled_by": "customer",
            "description": "string"
          }
        ],
        "dimensions": [
          {
            "key": "resolution",
            "controlled_by": "customer",
            "values": [
              "720p",
              "1080p"
            ],
            "description": "string"
          }
        ],
        "rate_card": {
          "currency": "usd",
          "rates": [
            {
              "unit": "video_seconds",
              "per": "1",
              "unit_amount_decimal": "12",
              "dimensions": {
                "resolution": "1080p"
              },
              "tier_mode": "graduated",
              "tiers": [
                {
                  "unit_amount_decimal": "300",
                  "up_to": "200000"
                }
              ]
            }
          ],
          "effective_from": "2024-01-15T09:30:00Z",
          "as_of": "2026-10-20",
          "source_reference": "Vidlab pricing page, read 20 October 2026"
        },
        "metadata": {}
      }
    ],
    "price_rules": [
      {
        "name": "string",
        "currency": "usd",
        "key": "video_cost_plus",
        "method": "cost_plus",
        "description": "string",
        "basis": "catalog",
        "inputs": [
          {
            "cost_model": "vidlab-v2",
            "unit": "video_seconds",
            "quantity_from": "quantity"
          }
        ],
        "markup_percent": "0.4",
        "target_margin_percent": "0.3",
        "rows": [
          {
            "dimensions": {
              "resolution": "1080p"
            },
            "unit_amount_decimal": "20",
            "tier_mode": "graduated",
            "tiers": [
              {
                "unit_amount_decimal": "300",
                "up_to": "200000"
              }
            ]
          }
        ],
        "formula": "max(cost * 1.4, 50)",
        "floor_decimal": "50",
        "cap_decimal": "string",
        "credit_value_decimal": "1",
        "on_cost_change": "hold",
        "effective_from": "2024-01-15T09:30:00Z"
      }
    ],
    "margin_guardrails": [
      {
        "name": "string",
        "min_margin_percent": "0.25",
        "key": "video_margin_floor",
        "applies_to": {
          "event_name": "video.generate",
          "outcome_definition": "string",
          "plan": "string"
        },
        "basis": "expected",
        "action": "warn",
        "window": "P7D",
        "enabled": true
      }
    ]
  },
  "cost_adjustments": [
    {
      "cost_model": "string",
      "unit": "string",
      "multiplier": "1.2",
      "catalog_price": "catp_9cW4hL6rBv"
    }
  ],
  "results": {
    "events_considered": 1,
    "costs_considered": 1,
    "events_unpriced": 1,
    "baseline": {
      "revenue_confirmed": "41200",
      "revenue_pending": "0",
      "revenue_reversed": "0",
      "cost": "27650",
      "margin": "13550",
      "margin_percent": "0.3289"
    },
    "candidate": {
      "revenue_confirmed": "41200",
      "revenue_pending": "0",
      "revenue_reversed": "0",
      "cost": "27650",
      "margin": "13550",
      "margin_percent": "0.3289"
    },
    "by_group": [
      {
        "group": "string",
        "baseline": {
          "revenue_confirmed": "41200",
          "revenue_pending": "0",
          "revenue_reversed": "0",
          "cost": "27650",
          "margin": "13550",
          "margin_percent": "0.3289"
        },
        "candidate": {
          "revenue_confirmed": "41200",
          "revenue_pending": "0",
          "revenue_reversed": "0",
          "cost": "27650",
          "margin": "13550",
          "margin_percent": "0.3289"
        }
      }
    ],
    "guardrails": [
      {
        "key": "string",
        "would_warn": 1,
        "would_block": 1
      }
    ],
    "warnings": [
      {
        "code": "confirmation_window_short",
        "message": "The charge confirms almost at once, so only the reversal horizon protects you.",
        "path": "outcome_definitions.0.confirmation_window"
      }
    ]
  },
  "error": {
    "type": "https://docs.runledger.co/reference/idempotency-and-errors#error-codes",
    "title": "Insufficient balance",
    "status": 402,
    "code": "invalid_request",
    "request_id": "req_7Hc2Lq0vTz8d",
    "retryable": true,
    "detail": "The hold needs 400 ai_credits but 120 are available.",
    "param": "events.3.timestamp",
    "required_scope": "config:write",
    "errors": [
      {
        "code": "confirmation_window_short",
        "message": "The charge confirms almost at once, so only the reversal horizon protects you.",
        "path": "outcome_definitions.0.confirmation_window"
      }
    ],
    "blocked_by": [
      {
        "object": "routing_rule",
        "id": "rr_3vT8nK2xWa"
      }
    ],
    "doc_url": "https://docs.runledger.co/reference/idempotency-and-errors#error-codes"
  },
  "actor": {
    "type": "user",
    "id": "usr_4tN8vB2cXk",
    "name": "Claude Code",
    "on_behalf_of": "usr_4tN8vB2cXk",
    "client": "claude-code"
  },
  "completed_at": "2024-01-15T09:30:00Z"
}
```

**SDK Code**

```python
import requests

url = "https://api.runledger.co/v1/simulations"

payload = {
    "from": "2024-01-15T09:30:00Z",
    "to": "2024-01-15T09:30:00Z",
    "currency": "usd"
}
headers = {
    "Authorization": "Bearer <api_key>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.runledger.co/v1/simulations';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <api_key>', 'Content-Type': 'application/json'},
  body: '{"from":"2024-01-15T09:30:00Z","to":"2024-01-15T09:30:00Z","currency":"usd"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.runledger.co/v1/simulations"

	payload := strings.NewReader("{\n  \"from\": \"2024-01-15T09:30:00Z\",\n  \"to\": \"2024-01-15T09:30:00Z\",\n  \"currency\": \"usd\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <api_key>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.runledger.co/v1/simulations")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <api_key>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"from\": \"2024-01-15T09:30:00Z\",\n  \"to\": \"2024-01-15T09:30:00Z\",\n  \"currency\": \"usd\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.runledger.co/v1/simulations")
  .header("Authorization", "Bearer <api_key>")
  .header("Content-Type", "application/json")
  .body("{\n  \"from\": \"2024-01-15T09:30:00Z\",\n  \"to\": \"2024-01-15T09:30:00Z\",\n  \"currency\": \"usd\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.runledger.co/v1/simulations', [
  'body' => '{
  "from": "2024-01-15T09:30:00Z",
  "to": "2024-01-15T09:30:00Z",
  "currency": "usd"
}',
  'headers' => [
    'Authorization' => 'Bearer <api_key>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.runledger.co/v1/simulations");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <api_key>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"from\": \"2024-01-15T09:30:00Z\",\n  \"to\": \"2024-01-15T09:30:00Z\",\n  \"currency\": \"usd\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <api_key>",
  "Content-Type": "application/json"
]
let parameters = [
  "from": "2024-01-15T09:30:00Z",
  "to": "2024-01-15T09:30:00Z",
  "currency": "usd"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.runledger.co/v1/simulations")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```