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

# Outcome settlement

An **outcome charge** is a charge that depends on a result, such as a resolved ticket or a completed task. Settlement decides whether it stands. In payments, "settlement" usually means money reaching a bank account. Here it doesn't: Runledger never moves money. It records the charge as pending, confirms it when the result stands, reverses it when it doesn't, and your payment provider collects whatever stands.

## The lifecycle

```mermaid
stateDiagram-v2
    [*] --> pending: track() reports the outcome
    pending --> confirmed: confirmation window closes, or confirm
    pending --> disputed: dispute opened
    pending --> reversed: reversal rule or reverse
    confirmed --> locked: reversal horizon closes
    confirmed --> disputed: dispute opened
    confirmed --> reversed: reversal rule or reverse
    disputed --> confirmed: upheld, horizon still open
    disputed --> locked: upheld, horizon passed while open
    disputed --> reversed: decided reverse, or deadline passes
    locked --> [*]
    reversed --> [*]
```

| Status      | Billed?                                                                          | Can still reverse?                                                 |
| ----------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `pending`   | No. A prepaid charge holds the credits; a postpaid charge stays off the invoice. | Yes                                                                |
| `disputed`  | Only if it was confirmed before the dispute opened.                              | Yes, by your decision, even if the horizon passes while it is open |
| `confirmed` | Yes. Prepaid credits are drawn; postpaid charges go on the current invoice.      | Yes, until `reversible_until`                                      |
| `locked`    | Yes, for good.                                                                   | No. Only a new, linked correction.                                 |
| `reversed`  | No, or credited back.                                                            | Already reversed                                                   |

Every change is recorded in the charge's `status_transitions` with its cause: the event that created it, a clock, a rule, a dispute or an API request.

## Two clocks

Each outcome definition sets two durations, and both are stamped on every charge it creates.

* **`confirmation_window`**: how long a new charge stays pending. If nothing happens, it confirms when the window closes. AI support agents typically use a few hours to a few days. Allowed range: `PT0S` to `P30D`.
* **`reversal_horizon`**: how long after confirmation the charge can still be reversed, for "was it really resolved?" cases such as a customer coming back a week later. When it closes, the charge locks, unless a [dispute is still open](#a-dispute-that-is-open-at-the-horizon). Allowed range: `PT0S` to `P180D`.

A charge confirms by default when nobody objects, the same default affiliate networks and card schemes use. Call `POST /v1/outcome_charges/{id}/confirm` to confirm early, for example when the end customer marks the conversation resolved.

### Presets, not defaults

The clocks have no silent default. Both stay required, and a definition always stores explicit values. To save you guessing, `GET /v1/outcome_definition_presets` returns a recommended set for common kinds of outcome. Send `preset` on `POST /v1/outcome_definitions` and the server fills in whichever of `confirmation_window`, `reversal_horizon` and `dispute_settings` you leave out, stores them on the version, and echoes them back with the preset's key. Values you send always win. Changing a preset later never changes a definition that used it. The dashboard shows the same presets as chips that fill in visible values, and an agent over MCP is told to ask you to confirm them.

| Preset           | Fits                                                                                      | Confirmation | Horizon | Why these values                                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------- | ------------ | ------- | -------------------------------------------------------------------------------------------------------------- |
| `support_chat`   | A support resolution in chat or messaging                                                 | `PT24H`      | `P30D`  | A day without a reply is a common sign that a chat is resolved, and a customer who comes back later reopens it |
| `support_email`  | A support resolution by email or another asynchronous channel                             | `PT72H`      | `P30D`  | Email replies arrive more slowly than chat, so silence takes three days to mean resolved                       |
| `voice_call`     | A voice call outcome                                                                      | `PT2H`       | `P14D`  | A call ends when it ends, so the confirmation window is short. The horizon covers a customer who calls back    |
| `qualified_lead` | A lead qualified or disqualified                                                          | `P7D`        | `P30D`  | A week gives a sales team time to accept or reject the lead                                                    |
| `completed_task` | A task checked against a system of record, such as a booking made or a document processed | `PT1H`       | `P14D`  | Short, because the system of record already confirms the result                                                |

The API returns each preset with a `rationale`, one sentence on what its values are based on, and a `confidence` of `low`, `medium` or `high`. Every preset also sets `decision_window` to `P7D` and `default_decision` to `reverse`. They are starting points: confirm them with your own customers and change what doesn't fit. The examples on this site use `support_chat`.

### Bounds and warnings

The hard limits above are enforced. Outside the recommended range a request still works, and the response carries `warnings` that say why. The dashboard and the MCP server show them too.

| Field                 | Hard limit        | Recommended       | Warning outside it                                                                                                                                       |
| --------------------- | ----------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `confirmation_window` | `PT0S` to `P30D`  | `PT1H` to `P7D`   | Under `PT1H` the charge confirms almost at once, so only the reversal horizon protects you. Over `P7D`, revenue waits over a week                        |
| `reversal_horizon`    | `PT0S` to `P180D` | `P7D` to `P90D`   | Over `P90D` a charge stays reversible past a quarter. A reversal after the invoice is final becomes a credit on the next invoice                         |
| `decision_window`     | `PT24H` to `P30D` | `PT24H` to `P14D` | Under `PT24H` is refused, because `dispute.decision_due` fires 24 hours before the deadline. Over `P14D`, card issuers expect a response in 7 to 21 days |

Validate a definition before you write it with `Runledger-Dry-Run: true`, or `POST /v1/config/validate`; see [Configuration](/api-reference/how-configuration-works).

## Outcome definitions are versioned

An outcome definition holds the `event_name` that reports the outcome, the `criteria` you agreed with your customers, the clocks, the reversal rules and the dispute settings. A plan sets its price.

Changing the criteria, clocks, rules or dispute settings creates a new version. Charges keep the terms of the version they were created under, and each charge records `outcome_definition_version`, so when a customer asks why they were charged, the answer is the definition they agreed to at the time. `GET /v1/outcome_definitions/{outcome_definition}/versions` lists every version with who made it, a summary of the change and the charge counts under it, and each version records its [actor](/api-reference/access-and-audit#who-did-it).

## Reversal rules

Most reversals should happen by rule, decided in advance, so nobody argues over each charge. A rule listens for a **signal event**: any `track()` event whose name a rule names. The signal finds its charge by customer and `reference`, so send the same reference (a conversation or ticket ID) on the outcome and on everything that follows.

```json
{
  "key": "low_csat",
  "event_name": "csat.submitted",
  "condition": { "property": "score", "operator": "lte", "value": "2" },
  "applies_to": "any",
  "action": "open_dispute",
  "reason_code": "customer_unhappy"
}
```

* `condition` tests one property of the signal event. Numbers compare numerically.
* `applies_to` limits the rule to `pending` charges, `confirmed` charges, or `any`.
* `action` is `reverse` (reverse at once) or `open_dispute` (put it to you).
* Rules run in order and the first match wins. Each definition takes up to 10.

A signal for a locked charge has no effect, and `track()` returns the warning `reversal_horizon_passed`. Use [`POST /v1/events/preview`](/api-reference/events/preview) to see which rule a signal would fire before you send it.

## Disputes

A dispute is the exception path, for complaints a rule didn't settle. Open one with `POST /v1/disputes` when an end customer complains through your support channel, or let an `open_dispute` rule do it.

* A dispute on a pending charge stops it confirming.
* You are the decider. Resolve with `uphold` (the charge stands; a pending charge confirms at once) or `reverse`.
* Each dispute has a `decide_by` deadline, set by the definition's `dispute_settings.decision_window` (at least 24 hours, at most 30 days). You send it with the definition, or a `preset` fills it in, as it does the two clocks; `support_chat` gives seven days. `dispute.decision_due` fires 24 hours before it, and a [notification rule](/api-reference/how-configuration-works#notification-rules) can email the people who decide.
* If nobody decides in time, `default_decision` applies. It defaults to `reverse`, so an ignored complaint doesn't stand. Set it to `uphold` if your terms say otherwise.

### A dispute that is open at the horizon

You can open a dispute until the reversal horizon passes. A dispute on a pending charge stops it confirming. A dispute on a confirmed charge holds its lock: if the horizon passes while the dispute is open, the charge stays `disputed` and doesn't lock until you decide.

* The charge carries an `open_dispute` block with the dispute's `id`, its `decide_by` and `holds_lock: true` when the dispute is what keeps the charge from locking.
* `reverse` reverses the charge, even though the horizon has passed.
* `uphold` locks it at once if the horizon passed while the dispute was open, with `lock_reason: dispute_upheld`, which the dashboard shows as "locked after dispute". If the horizon hasn't passed, an upheld charge goes back to `confirmed` and locks when the horizon closes, with `lock_reason: reversal_horizon`.
* If nobody decides, `default_decision` applies at `decide_by`, which can be after the horizon.

Opening a dispute on a charge that has already locked is refused.

Provider chargebacks on top-ups and invoice payments are a different thing: they happen at your payment provider and don't change an outcome charge. See [Payment providers](/concepts/payment-providers#chargebacks-on-top-ups).

## What a reversal does

A reversal is its own immutable object, and `GET /v1/reversals` lists them. It records the reason, the source (`rule`, `dispute` or `api`), the `actor` when a person or an agent made it, and the effect on money:

| The charge was                        | Billing mode | Effect                 | What happens                                                        |
| ------------------------------------- | ------------ | ---------------------- | ------------------------------------------------------------------- |
| Pending or disputed before confirming | Either       | `reservation_released` | Nothing was billed; the held amount is released.                    |
| Confirmed                             | Prepaid      | `credits_restored`     | The credits come back with a new ledger entry linked to the charge. |
| Confirmed, invoice still a draft      | Postpaid     | `accrual_removed`      | The charge comes off the draft invoice.                             |
| Confirmed and already invoiced        | Postpaid     | `invoice_credit`       | The next invoice carries a credit line for it.                      |

Runledger reverses the charge itself and never edits a finalized invoice. A reversal applies to a whole charge; there are no partial reversals.

## One charge per reference

A customer can have only one active outcome charge per outcome definition and reference. A second outcome event for the same conversation while the first charge is pending, disputed, confirmed or locked creates nothing, and `track()` warns `duplicate_outcome_for_reference`. Once a charge is reversed, a later outcome event for the same reference creates a new charge, for example when a reopened conversation is resolved again.

## Evidence

Attach `evidence` to the outcome event: a short description, a link to the transcript, and properties such as a satisfaction score. It is stamped on the charge with the definition version and shown to whoever decides a dispute. Keep it factual. Free text is sanitized before it reaches dashboards or agent tools.

## For your finance team

[`GET /v1/reports/revenue`](/api-reference/reports/revenue) splits each period into recognized, pending, reversible, reversed, deferred and breakage amounts, splits recognized revenue by invoice line type (usage, outcome, overage and minimum commitment), can return the same figures by day, week or month, and each outcome definition reports its amounts and its `reversal_rate` over the range you ask for. [`GET /v1/reports/outcomes`](/api-reference/reports/outcomes) gives counts and amounts over time, grouped by status, definition, reason or source, so reversal and dispute rates don't depend on adding up list pages. Credits and money are reported separately and never added together. These are operating figures. Confirm the accounting treatment, such as how much of the reversible amount to hold back from revenue, with your adviser.

## Limits

* **Reversals are whole-charge.** You can't reverse part of a charge, such as half for an escalated conversation.
* **Who records disputes.** You and your team record complaints with `POST /v1/disputes`. Your end customers don't open disputes themselves, and the [customer view](/guides/customer-view) has no dispute screen.
* **Who decides.** Disputes are decided by you and your team. A third party can't be named as the decider.
* **Where settlement runs.** Settlement uses Runledger's own plans and invoices. It doesn't run on top of another billing system.
* **Outcome charges** keep the price on their outcome definition. [Price rules](/guides/price-rules-and-guardrails) apply to usage, not to outcome charges.

## Next steps

* [Checks and holds](/concepts/checks-and-holds) decide work before it starts, and hold what it will cost.
* [Webhooks](/api-reference/handle-webhooks) tell you when a charge confirms, reverses or locks, and when a dispute is due.
* [Sandbox and test clocks](/api-reference/test-in-the-sandbox) lets you run the whole lifecycle in seconds.