Skip to navigation

Outcome settlement

The life of an outcome charge, and the rules that decide whether it stands.

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

track() reports the outcome confirmation window closes, or confirm dispute opened reversal rule or reverse reversal horizon closes dispute opened reversal rule or reverse upheld, horizon still open upheld, horizon passed while open decided reverse, or deadline passes pending confirmed disputed reversed locked
StatusBilled?Can still reverse?
pendingNo. A prepaid charge holds the credits; a postpaid charge stays off the invoice.Yes
disputedOnly if it was confirmed before the dispute opened.Yes, by your decision, even if the horizon passes while it is open
confirmedYes. Prepaid credits are drawn; postpaid charges go on the current invoice.Yes, until reversible_until
lockedYes, for good.No. Only a new, linked correction.
reversedNo, 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. 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.

PresetFitsConfirmationHorizonWhy these values
support_chatA support resolution in chat or messagingPT24HP30DA day without a reply is a common sign that a chat is resolved, and a customer who comes back later reopens it
support_emailA support resolution by email or another asynchronous channelPT72HP30DEmail replies arrive more slowly than chat, so silence takes three days to mean resolved
voice_callA voice call outcomePT2HP14DA call ends when it ends, so the confirmation window is short. The horizon covers a customer who calls back
qualified_leadA lead qualified or disqualifiedP7DP30DA week gives a sales team time to accept or reject the lead
completed_taskA task checked against a system of record, such as a booking made or a document processedPT1HP14DShort, 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.

FieldHard limitRecommendedWarning outside it
confirmation_windowPT0S to P30DPT1H to P7DUnder PT1H the charge confirms almost at once, so only the reversal horizon protects you. Over P7D, revenue waits over a week
reversal_horizonPT0S to P180DP7D to P90DOver P90D a charge stays reversible past a quarter. A reversal after the invoice is final becomes a credit on the next invoice
decision_windowPT24H to P30DPT24H to P14DUnder 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.

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.

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.

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

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 wasBilling modeEffectWhat happens
Pending or disputed before confirmingEitherreservation_releasedNothing was billed; the held amount is released.
ConfirmedPrepaidcredits_restoredThe credits come back with a new ledger entry linked to the charge.
Confirmed, invoice still a draftPostpaidaccrual_removedThe charge comes off the draft invoice.
Confirmed and already invoicedPostpaidinvoice_creditThe 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 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 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 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 apply to usage, not to outcome charges.

Next steps