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

# Conventions

The same rules apply to every endpoint.

## Requests and responses

* JSON bodies, `snake_case` fields, HTTPS only.
* One API host for both environments, `https://api.runledger.co`. The key decides whether a request runs in the sandbox or live, and every object carries `livemode`. The dashboard is at `https://app.runledger.co`, the developer MCP server at `https://mcp.runledger.co/mcp`, and the hosted customer view at `https://billing.runledger.co/s/{session}`.
* Updates use `PATCH` and change only the fields you send. Send `null` to clear a field. The one exception is `PUT /v1/config`, which takes a whole [configuration document](/api-reference/how-configuration-works).
* Config writes accept `Runledger-Dry-Run: true` to show what they would do without doing it. See [Configuration](/api-reference/how-configuration-works#dry-run). Every `check()` response carries an `affordability` block, with no header needed. See [Checks and holds](/concepts/checks-and-holds#how-far-the-work-fits).
* Fields with no value are left out of responses, never sent as `null`, except the ones the reference documents as nullable, such as the figures in `affordability` and `margin` on a run.
* State changes are explicit actions on the object, such as `POST /v1/holds/{id}/release` or `POST /v1/outcome_charges/{id}/reverse`. Status fields can't be written directly.

## IDs

Every object ID is a string with a type prefix, so you can tell what an ID refers to by looking at it. Treat the part after the prefix as opaque; its length may change.

| Prefix   | Object             | Prefix  | Object                |
| -------- | ------------------ | ------- | --------------------- |
| `cus_`   | Customer           | `plan_` | Plan                  |
| `ct_`    | Credit type        | `sub_`  | Subscription          |
| `cg_`    | Credit grant       | `inv_`  | Invoice               |
| `le_`    | Ledger entry       | `il_`   | Invoice line          |
| `hold_`  | Hold               | `cs_`   | Checkout session      |
| `ev_`    | Event              | `cost_` | Cost                  |
| `od_`    | Outcome definition | `exp_`  | Export                |
| `oc_`    | Outcome charge     | `rec_`  | Reconciliation        |
| `rev_`   | Reversal           | `we_`   | Webhook endpoint      |
| `dsp_`   | Dispute            | `whe_`  | Webhook event         |
| `clock_` | Test clock         | `req_`  | Request               |
| `acct_`  | Account            | `key_`  | API key               |
| `pp_`    | Payment provider   | `pay_`  | Payment               |
| `rr_`    | Routing rule       | `nr_`   | Notification rule     |
| `cprop_` | Config proposal    | `ae_`   | Audit event           |
| `ic_`    | Integration check  | `cm_`   | Cost model            |
| `rc_`    | Rate card          | `pr_`   | Price rule            |
| `mg_`    | Margin guardrail   | `sim_`  | Simulation            |
| `catm_`  | Catalog model      | `catp_` | Catalog price version |
| `sl_`    | Spend limit        | `atu_`  | Auto top-up           |
| `csess_` | Customer session   |         |                       |

Pools and allowances are named by a `key` you choose inside a plan, such as `included` or `weekly`, and have no ID of their own. `member_id`, `team_id` and `agent_id` are your own IDs, up to 128 characters, that you send on calls to attribute usage. See [Members, teams and agents](/guides/members-teams-agents).

Customers also take your own ID as `external_id`. Calls made on a customer's behalf, such as `check()`, `track()` and `checkout()`, accept either `customer_id` or `external_customer_id`. URL paths and list filters take Runledger's `cus_` ID, which you can look up from yours with `GET /v1/customers?external_id=`. Credit types, plans and outcome definitions have a `key` you choose, such as `chat_resolution`, and requests accept the key or the ID.

## Money, credits and quantities

* **Money** is an integer in the currency's minor unit with a lowercase ISO 4217 code: `{"amount": 1500, "currency": "usd"}` is US\$15.00. Zero-decimal currencies follow ISO 4217, so `{"amount": 1500, "currency": "jpy"}` is ¥1,500.
* **Unit prices in money** can be fractions of a minor unit, so they are decimal strings in minor units: `unit_amount_decimal: "95"` is US\$0.95, and `"0.042"` is 0.042 US cents.
* **Credits and quantities** are decimal strings, such as `"12.5"`, with up to the credit type's `decimal_places`.
* **Rounding** happens once, on each invoice line, half up to the minor unit. Ledger amounts are never rounded.

## Time

* Timestamps are RFC 3339 strings in UTC: `2026-11-02T09:00:00Z`.
* Durations are ISO 8601: `PT15M`, `PT24H`, `P30D`.
* Ledger entries carry both `effective_at` (when it happened in business terms) and `posted_at` (when Runledger recorded it).

## Lists

List endpoints return the newest objects first:

```json
{ "object": "list", "data": [ ... ], "has_more": true, "next_cursor": "c_9f2..." }
```

Pass `next_cursor` back as `cursor` for the next page. `limit` is 1 to 100, default 20. Cursors are opaque and don't grant access to anything. The SDKs page automatically:

```typescript
for await (const charge of await runledger.outcomeCharges.list({ status: "disputed" })) {
  console.log(charge.id);
}
```

Lists don't return a total count unless you ask. Add `include=total_count` to the main lists (events, outcome charges, customers, disputes, reversals, request logs and webhook events) and the response carries `total_count`, capped, with `total_count_approximate: true` when the real number is higher. For counts and amounts over time, use [`GET /v1/reports/outcomes`](/api-reference/reports/outcomes) instead of adding up pages. Reads of a single object always reflect your writes; lists may lag by a few seconds, so don't use a list to confirm a write you just made.

Routing rules are the one list that isn't newest first: they come in order, first match first.

## Metadata

Most objects take `metadata`: up to 50 keys of your own, keys up to 40 characters and values up to 500. Don't put personal data or secrets in it.

## Versioning

Send `Runledger-Version: 2026-10-18` on every request; the SDKs do this for you. Without it, a request uses the version your account is pinned to, which you can read from `GET /v1/account`.

Within a version, changes are additive only: new endpoints, new optional fields, new enum values, new error and event types. Handle values you don't recognize, and ignore fields you don't use. A change that removes or renames anything, adds a required field, or changes a type ships as a new dated version, and old versions get advance notice through `Deprecation` and `Sunset` headers.

## Rate limits

A request over the limit gets `429` with a `Retry-After` header in seconds. The SDKs wait and retry. Limits are counted per account, per environment and per endpoint, in one-second windows. `check()`, `track()`, the event batch and `trace()` have a higher limit than the rest, and live's limits are above the sandbox's: 100 requests a second for most endpoints and 1,000 for those four in live, 25 and 100 in the sandbox. A person's calls from the dashboard and the customer view's are not counted.

## Keys

| Prefix                       | Key                                                                                   |
| ---------------------------- | ------------------------------------------------------------------------------------- |
| `rl_sk_test_`, `rl_sk_live_` | Secret key: full access to its environment                                            |
| `rl_rk_test_`, `rl_rk_live_` | Restricted key: only the scopes you grant. It can carry an expiry and an `agent` flag |

Keys end in a checksum so secret scanners can recognize a leaked key, and a leaked key reported to us is revoked. People create, roll and revoke keys in the [dashboard](/dashboard); when you roll a key, the old one keeps working for up to seven days. `GET /v1/api_keys` lists them, masked, with their scopes, so an agent can see what it holds. Agents never create or roll keys. Never use a key in a browser or mobile app.

A restricted key's [scopes](/api-reference/access-and-audit#keys-and-scopes) are checked on every call. One it lacks returns `403` with `insufficient_scope` and `required_scope`.

## Actors

Every change records who made it, as an `actor` with a `type` of `user`, `agent`, `key`, `rule` or `system`. Runledger fills it in from the credential. See [Access, actors and audit](/api-reference/access-and-audit#who-did-it).

## Request IDs

Every response has a `Request-Id` header, and every error body has a `request_id`. Look a request up with `GET /v1/request_logs/{request_id}`, or quote it to support.

## SDKs and tools

Runledger has SDKs for TypeScript and Python, generated from the [OpenAPI spec](/api-reference). Both set the `Runledger-Version` header, wait and retry on `429`, and expose `check()`, `track()`, `trace()` and `checkout()`. `trace()` records the model, provider and units you pass it; there are no wrappers that record costs from a model client automatically. Webhook signatures are verified with any [Standard Webhooks](https://www.standardwebhooks.com) library. There is no CLI. Keys are created, rolled and revoked in the dashboard, not through the API.

## See also

* [Glossary](/reference/glossary) defines the terms used across the docs.
* [Idempotency and errors](/reference/idempotency-and-errors) covers retries and the error body.