Skip to navigation

Provider price changes and simulation

Decide whether your prices follow a provider's new price, and replay recorded usage against any change before you apply it.

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.

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

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:

{
"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 on the same event emails people. The rate card shows what changed and what it touches:

{
"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 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 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":

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

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:

{
"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