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

# Summarize runs

GET https://api.runledger.co/v1/runs/summary

Total revenue, cost and margin across all the runs that match the filters, not just a page of them. The filters are the ones on `GET /v1/runs`: `customer_id`, `outcome_definition`, `from`, `to` and `has_margin`.

**Split for a chart.** Send `group_by` to also get `rows`, with the same totals broken down. `day` and `week` give one row per bucket, oldest first, by the time a run was first seen (UTC; a week starts on Monday). Every bucket from the first to the last one that has a run is returned, an empty one with zeros, so a chart has no gaps. `customer` gives one row per customer, the highest `revenue_confirmed` first, up to `limit`. The totals at the top level always cover every matching run, not just the rows returned. Each row has `revenue_confirmed`, `cost`, `margin`, `margin_percent` and `has_margin`, counted as for the totals. A row with no run that has a margin has `has_margin: false` and a `null` `margin` and `margin_percent`, so a chart can leave it out instead of plotting zero. The filters and the one-currency rule are the same with or without `group_by`.

**Runs without a margin.** The money totals add up only the runs that have a margin, which needs a confirmed charge and a traced cost, so `margin` is always `revenue_confirmed` minus `cost`. A run with no confirmed charge (nothing, or only pending or reversed charges) or with no traced cost adds nothing to `revenue_confirmed`, `cost` or `margin`, whichever of the two it is missing. It is counted in `runs` and in `runs_without_margin`, so you can see how many were left out. With `has_margin=false` every matching run is without a margin, so the totals are `"0"`.

One currency per summary: totals are never converted or added across currencies. Send `currency`, or omit it when every matching run is in one currency. If the matching runs are in more than one, the call returns `400` with `parameter_invalid` on `currency`.

Reference: https://docs.runledger.co/api-reference/runs/summary

## Authentication

- `Authorization` header (bearer token, required) — A secret key (`rl_sk_test_...`, `rl_sk_live_...`) or a restricted key with limited scopes (`rl_rk_test_...`, `rl_rk_live_...`). A restricted key can carry `agent: true` and an `expires_at`, and is what a coding agent should hold. Keys end in a checksum so secret scanners can recognize them. People create, roll and revoke keys in the dashboard; `GET /v1/api_keys` lists them, masked.

## Request

### Query parameters

- `customer_id` (string, optional) — Only objects belonging to this customer.
- `outcome_definition` (string, optional) — Only runs that produced a charge for this outcome definition (ID or key).
- `from` (datetime, optional) — Only runs first seen at or after this time.
- `to` (datetime, optional) — Only runs first seen before this time.
- `has_margin` (boolean, optional) — As on `GET /v1/runs`: only runs that have a margin (`true`), or only runs that don't (`false`). Omit it for both.
- `currency` (string, optional) — Summarize runs in this currency (lowercase ISO 4217). Needed when the matching runs are in more than one.
- `group_by` (enum, optional) — Also return `rows`: one per `day`, one per `week`, or one per `customer`. Leave it out for the totals alone.
  - Allowed values: `day`, `week`, `customer`
- `limit` (integer, optional, default: 50) — `group_by=customer` only: the most customer rows to return, from 1 to 500. Default 50.

## Response

### 200

The totals, and the rows when `group_by` is sent.

- `object` ("runs_summary", required)
- `currency` (string, required)
- `runs` (integer, required) — How many runs match the filters. `runs_with_margin` plus `runs_without_margin`.
- `runs_with_margin` (integer, required) — Matching runs with a confirmed charge and a traced cost.
- `runs_without_margin` (integer, required) — Matching runs with no confirmed charge or no traced cost. They add nothing to the money totals.
- `revenue_confirmed` (string, required) — Decimal minor units. The confirmed revenue of the runs with a margin. `"0"` when there are none.
- `cost` (string, required) — Decimal minor units. The cost of the runs with a margin. The cost of a run without a margin is not included. `"0"` when there are none.
- `margin` (string, required) — Decimal minor units. `revenue_confirmed` minus `cost`. `"0"` when no run has a margin.
- `margin_percent` (string, required, nullable) — `margin` as a share of `revenue_confirmed`. `null` when no run has a margin.
- `livemode` (boolean, required)
- `group_by` (enum, optional) — Present when the request sent `group_by`.
  - Allowed values: `day`, `week`, `customer`
- `rows` (list of RunSummaryRow, optional) — Present when the request sent `group_by`. The same totals, split by bucket or by customer.
- `has_more` (boolean, optional) — `group_by=customer` only: `true` when more customers matched than `limit` returned.

## Errors

### 400 Bad Request Error

The request was malformed or a parameter was invalid. A configuration that fails validation, such as two equally specific overrides with overlapping dates, returns `config_invalid`.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 401 Unauthorized Error

The API key is missing, invalid or revoked.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 403 Forbidden Error

The key can't do this. A restricted or agent key lacks the scope (`insufficient_scope`, with `required_scope`), a live key called a sandbox-only endpoint, an agent tried a live config write that needs a proposal (`proposal_required`), or something only a signed-in person can do.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 429 Too Many Requests Error

Too many requests. Wait for the number of seconds in `Retry-After`.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

### 500 Internal Server Error

Something went wrong on Runledger's side. Safe to retry with the same idempotency key.

- `type` (string, required) — A URI for the error type. It links to the error codes table in the docs, where each `code` is listed with its cause. Branch on `code`, not on `type`.
- `title` (string, required) — A short summary of the error type.
- `status` (integer, required) — The HTTP status code.
- `code` (enum, required) — A stable, machine-readable error code. New codes may be added.
  - Allowed values: `invalid_request`, `parameter_missing`, `parameter_invalid`, `parameter_unknown`, `authentication_failed`, `permission_denied`, `insufficient_scope`, `proposal_required`, `config_invalid`, `livemode_mismatch`, `resource_not_found`, `resource_already_exists`, `idempotency_key_in_use`, `idempotency_key_reused`, `invalid_state`, `outcome_charge_locked`, `hold_not_active`, `insufficient_balance`, `credit_limit_exceeded`, `plan_locked`, `test_clock_advancing`, `provider_error`, `rate_limited`, `api_error`, `service_unavailable`
- `request_id` (string, required) — The ID of the request. Quote it to support, or look it up in request logs.
- `retryable` (boolean, required) — Whether sending the same request again, with the same idempotency key, may succeed.
- `detail` (string, optional) — An explanation of this occurrence. For people; don't parse it.
- `param` (string, optional) — The request parameter at fault, as a dotted path.
- `required_scope` (string, optional) — The scope the call needed, on `insufficient_scope`. Ask a person to widen the key, or reconnect with it.
- `errors` (list of ConfigIssue, optional) — Only with `config_invalid`: each problem the configuration document has, with the path of the part at fault, so an agent can fix the document without a second call.
- `blocked_by` (list of ErrorResponseBlockedByItems, optional) — On an `invalid_state` that names the objects in the way, the objects that must change first. Removing a payment provider lists every routing rule that uses it (`object: routing_rule`) and every customer whose own `payment_provider` names it (`object: customer`), in one list.
- `doc_url` (string, optional) — A link to the error codes table in the docs, where this `code` is listed with its cause.

## Types

### RunSummaryRow

One bucket or one customer of a run summary. Counted as the totals are: the money fields cover only the runs that have a margin.

- `runs` (integer, required) — Matching runs in the row.
- `runs_with_margin` (integer, required)
- `runs_without_margin` (integer, required)
- `revenue_confirmed` (string, required) — Decimal minor units. The confirmed revenue of the row's runs that have a margin. `"0"` when there are none.
- `cost` (string, required) — Decimal minor units. The cost of the row's runs that have a margin. `"0"` when there are none.
- `margin` (string, required, nullable) — Decimal minor units. `revenue_confirmed` minus `cost`. `null` when `has_margin` is `false`.
- `margin_percent` (string, required, nullable) — `margin` as a share of `revenue_confirmed`, from 0 up to 1. `null` when `has_margin` is `false`.
- `has_margin` (boolean, required) — `true` when at least one run in the row has a margin. A row with `has_margin: false` isn't a zero margin: it has no margin data.
- `period_start` (datetime, optional) — `day` and `week` rows: the start of the bucket, inclusive.
- `period_end` (datetime, optional) — `day` and `week` rows: the end of the bucket, exclusive.
- `customer` (string, optional) — `customer` rows: the customer ID.

### ConfigIssue

A problem or warning about a part of a configuration document.

- `code` (string, required) — A stable code. New codes may be added.
- `message` (string, required)
- `path` (string, optional) — Where in the document, as a dotted path.

### ErrorResponseBlockedByItems

- `object` (enum, required) — The kind of object in the way: a routing rule, or a customer with a `payment_provider` override.
  - Allowed values: `routing_rule`, `customer`
- `id` (string, required) — The object's ID, `rr_...` for a routing rule and `cus_...` for a customer.

## Examples

**Response**

```json
{
  "object": "string",
  "currency": "usd",
  "runs": 412,
  "runs_with_margin": 371,
  "runs_without_margin": 41,
  "revenue_confirmed": "35240",
  "cost": "9112.5",
  "margin": "26127.5",
  "margin_percent": "0.7414",
  "livemode": true,
  "group_by": "day",
  "rows": [
    {
      "runs": 58,
      "runs_with_margin": 52,
      "runs_without_margin": 6,
      "revenue_confirmed": "4980",
      "cost": "1204.5",
      "margin": "3775.5",
      "margin_percent": "0.758",
      "has_margin": true,
      "period_start": "2024-01-15T09:30:00Z",
      "period_end": "2024-01-15T09:30:00Z",
      "customer": "cus_9sF2kq81LmQz"
    }
  ],
  "has_more": true
}
```

**SDK Code**

```python
import requests

url = "https://api.runledger.co/v1/runs/summary"

querystring = {"currency":"usd"}

headers = {"Authorization": "Bearer <api_key>"}

response = requests.get(url, headers=headers, params=querystring)

print(response.json())
```

```javascript
const url = 'https://api.runledger.co/v1/runs/summary?currency=usd';
const options = {method: 'GET', headers: {Authorization: 'Bearer <api_key>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.runledger.co/v1/runs/summary?currency=usd"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("Authorization", "Bearer <api_key>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.runledger.co/v1/runs/summary?currency=usd")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <api_key>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.runledger.co/v1/runs/summary?currency=usd")
  .header("Authorization", "Bearer <api_key>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.runledger.co/v1/runs/summary?currency=usd', [
  'headers' => [
    'Authorization' => 'Bearer <api_key>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.runledger.co/v1/runs/summary?currency=usd");
var request = new RestRequest(Method.GET);
request.AddHeader("Authorization", "Bearer <api_key>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Authorization": "Bearer <api_key>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.runledger.co/v1/runs/summary?currency=usd")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```