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

# Provider price changes and simulation

Provider prices change. This guide shows how a catalog update reaches your cost models, the policy that decides what happens to your prices, and how to replay recorded events and costs against a candidate before it goes live.

> **Note**
>
> Every amount in the examples is illustrative, and so are the providers (Vidlab, Textco) and the catalog entries shown: none is a real provider price.

## Before you start

* You know the [parts of pricing from cost](/concepts/cost-reflective-pricing#the-pieces) and who controls each variable.
* You have a [cost model](/guides/cost-catalog-and-models) and a [price rule](/guides/price-rules-and-guardrails), and some recorded usage and costs to replay.

## When a provider changes its prices

Providers change prices, sometimes on a date they announce in advance. Runledger keeps three things apart: the rates you pay, what you charge, and history.

**The catalog moves for you.** When a provider changes a price, Runledger publishes a new catalog price version with the date it starts, its source and what changed:

```json
{
  "id": "catp_9cW4hL6rBv",
  "object": "catalog_price",
  "catalog_model": "catm_5xT7qD2mKa",
  "version": 2,
  "status": "scheduled",
  "currency": "usd",
  "rates": [
    { "unit": "video_seconds", "unit_amount_decimal": "6", "dimensions": { "resolution": "720p" } },
    { "unit": "video_seconds", "unit_amount_decimal": "17", "dimensions": { "resolution": "1080p" } }
  ],
  "effective_from": "2026-11-01T00:00:00Z",
  "as_of": "2026-10-24",
  "source_url": "https://vidlab.example/pricing-notice",
  "source_reference": "Vidlab price change notice, 24 October 2026",
  "changes": [
    { "path": "rates.1.unit_amount_decimal", "before": "12", "after": "17" }
  ],
  "published_at": "2026-10-24T11:05:12Z",
  "last_verified_at": "2026-10-24T11:05:12Z"
}
```

Every account with a cost model on that catalog model gets a matching rate card straight away, `scheduled` until `effective_from`, and a `catalog.price_changed` webhook. A [notification rule](/api-reference/how-configuration-works#notification-rules) on the same event emails people. The rate card shows what changed and what it touches:

```json
{
  "id": "rc_7vN2dQ4mHs",
  "object": "rate_card",
  "cost_model": "cm_6bR3xN8vQe",
  "version": 2,
  "status": "scheduled",
  "source": "catalog",
  "catalog_price": "catp_9cW4hL6rBv",
  "currency": "usd",
  "rates": [
    { "unit": "video_seconds", "unit_amount_decimal": "6", "dimensions": { "resolution": "720p" } },
    { "unit": "video_seconds", "unit_amount_decimal": "17", "dimensions": { "resolution": "1080p" } }
  ],
  "effective_from": "2026-11-01T00:00:00Z",
  "as_of": "2026-10-24",
  "source_reference": "Vidlab price change notice, 24 October 2026",
  "source_url": "https://vidlab.example/pricing-notice",
  "changes": [
    { "path": "rates.1.unit_amount_decimal", "before": "12", "after": "17" }
  ],
  "affected_price_rules": ["video_cost_plus"],
  "warnings": [
    { "code": "price_rule_held", "message": "video_cost_plus is set to hold, so customer prices stay as they are and the margin on 1080p falls." }
  ],
  "livemode": false,
  "created_at": "2026-10-24T11:05:12Z"
}
```

* **Your costs follow the catalog from its date.** Costs and events dated before `effective_from` keep the version that priced them, and nothing already recorded is repriced. Your overrides still apply: a `unit_amount_decimal` override keeps its rate when the catalog moves, and a `discount_percent` override is taken off the new catalog rate.
* **What you charge follows your policy, not the catalog.** Each price rule has `on_cost_change`. With `hold`, the default, the rule keeps pricing from the rate card versions in force when you last saved it (`priced_from` on the rule shows them), so customer prices don't move and the margin does. A guardrail then tells you when it crosses the floor. With `follow`, the price follows the new card from its date. Under `hold`, repricing is a deliberate step: a person updates the rule, which makes a new version, ideally with an `effective_from` that gives customers notice. The default is `hold` so that an update to the catalog never changes a customer's price without a person choosing it.
* **Simulate before you choose.** [Replay](#simulation) recent usage against the new rates under each policy, naming the new catalog price in `cost_adjustments`.
* **A correction is a new version too.** If Runledger finds a catalog price was wrong, it publishes a corrected version that takes effect from the date it is published, never from an earlier date. Like every rate card version, it can't start in the past. Costs already recorded are not repriced, and the `changes` list says what moved.

### Decide what happens to your prices

In the dashboard, a catalog price change opens a review in four steps: what changed, what it touches, simulate first, and decide for each price rule it touches. You choose one of three for each rule:

* **Hold** is the default. Customer prices stay where they are and the margin moves. Holding writes no change to the rule, so it has an audit event of its own: `catalog.price_change_held`, with the `actor` who held it and the price change (`catp_...`). The choice, who made it and when, shows on the review. Through the API it is `POST /v1/catalog/prices/{catalog_price}/hold` with the `price_rule` and an optional `note`; a rule already on `follow` is refused with `409` and `invalid_state`, since it reprices from the new card's date anyway. Read every decision on a change with `GET /v1/audit_events?resource={catalog_price}`.
* **Follow** sets `on_cost_change` to `follow`, with an `effective_from` that gives customers notice.
* **Reprice** opens the rule to edit, which makes a new version of the rule that applies from its `effective_from` onward. It changes what you charge from then, never usage already recorded.

An Admin decides in live. A Collaborator can review it, run the simulation, and propose the decision for an Admin to approve. A Collaborator's hold on a rule that is already on hold is recorded at once, since it changes no price. There is no separate un-hold: a held rule is changed by deciding again. Choosing *follow* or *reprice* is an ordinary price rule update, recorded in the [audit log](/api-reference/access-and-audit#the-audit-log) as one, and choosing *hold* again records another `catalog.price_change_held`. Costs already recorded keep their rates whatever you choose.

**A custom cost model changes the old way.** You add the rates yourself, as a rate card with the date they start. The response is the same shape with `source: "custom"`:

```json
{
  "cost_model": "selfhost-llm",
  "currency": "usd",
  "effective_from": "2026-11-01T00:00:00Z",
  "as_of": "2026-10-24",
  "source_reference": "Cluster cost for October 2026 divided by GPU seconds used",
  "rates": [
    { "unit": "gpu_seconds", "unit_amount_decimal": "0.42" }
  ],
  "reason": "GPU hour price rose with the new cluster contract"
}
```

**Rules for a new rate card version.** It must start after every earlier version of the cost model, including one that is still scheduled, and its `effective_from` can't be in the past. A request that breaks either rule returns `400` with `parameter_invalid` on `effective_from`. A new version applies only from its start date onward. Recorded usage is never repriced: usage already recorded keeps the version that priced it.

A draft rate card (one created without `effective_from`) can be activated with `POST /v1/rate_cards/{id}/activate`, under the same two rules. The `rate_card.activated` webhook fires when a card becomes active. In live, an agent can't write a rate card or an override directly, so it proposes one like any other configuration change. Every change is in the [audit log](/api-reference/access-and-audit#the-audit-log) with who made it.

## Simulation

A simulation replays recorded events and costs from a past range against a candidate: some new configuration, some cost adjustments, or both. It writes nothing to balances, invoices or live configuration.

```bash
curl -X POST https://api.runledger.co/v1/simulations \
  -H "Authorization: Bearer $RUNLEDGER_API_KEY" \
  -H "Runledger-Version: 2026-10-18" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"from": "2026-10-01T00:00:00Z", "to": "2026-10-22T00:00:00Z", "currency": "usd",
       "group_by": "model",
       "cost_adjustments": [{"cost_model": "vidlab-v2", "multiplier": "1.2"}]}'
```

The result sets the margin the range had next to the margin it would have had:

```json
{
  "id": "sim_5wD2nF8qTj",
  "object": "simulation",
  "status": "completed",
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-22T00:00:00Z",
  "currency": "usd",
  "results": {
    "events_considered": 5120,
    "costs_considered": 9871,
    "events_unpriced": 0,
    "baseline": {
      "revenue_confirmed": "41200", "revenue_pending": "1900", "revenue_reversed": "2100",
      "cost": "27650", "margin": "13550", "margin_percent": "0.3289"
    },
    "candidate": {
      "revenue_confirmed": "41200", "revenue_pending": "1900", "revenue_reversed": "2100",
      "cost": "32450", "margin": "8750", "margin_percent": "0.2124"
    },
    "guardrails": [{ "key": "video_margin_floor", "would_warn": 0, "would_block": 312 }]
  },
  "livemode": false,
  "created_at": "2026-10-24T11:10:03Z",
  "completed_at": "2026-10-24T11:10:09Z"
}
```

* Costs priced from your cost models are priced again from their usage lines. Costs you reported yourself with `cost` stay as reported.
* A cost adjustment can be a `multiplier`, or a `catalog_price` for a catalog cost model: replay the range with the lines priced from that catalog price version, such as one scheduled for next month. Your overrides still apply.
* Outcome charges and their reversals stay as recorded, so the margin still counts reversals.
* A candidate `document` can carry cost models, price rules and guardrails, which replace the objects with the same key for the simulation only.
* A range is at most 31 days. A simulation can return `pending`; fetch it with `GET /v1/simulations/{id}`, or wait for `simulation.completed`.
* It works with sandbox and live keys, because it only reads.

## Next steps

* [Catalog, cost models and overrides](/guides/cost-catalog-and-models) covers the objects a price change touches.
* [Configuration](/api-reference/how-configuration-works) covers the dry run, apply and proposal steps that follow a simulation.