> For the complete documentation index, see [llms.txt](https://docs.robotmoney.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.robotmoney.com/platform/ledger-api.md).

# Ledger API

This page is optional for headless integrations.

Query the deterministic structured records the RobotMoney protocol produces from Raw evidence. The Ledger is the single source of financial truth. Every row carries provenance, evidence references, and a confidence score.

| Operation                         | Endpoint                                                             |
| --------------------------------- | -------------------------------------------------------------------- |
| List accounts                     | `GET /v1/ledger/accounts`                                            |
| Account detail (+ latest balance) | `GET /v1/ledger/accounts/{account_id}`                               |
| List balances (point-in-time)     | `GET /v1/ledger/balances`                                            |
| List counterparties               | `GET /v1/ledger/counterparties`                                      |
| Create a manual counterparty      | `POST /v1/ledger/counterparties`                                     |
| Update counterparty identity      | `PATCH /v1/ledger/counterparties/{counterparty_id}`                  |
| Grant counterparty trust          | `POST /v1/ledger/counterparties/{counterparty_id}/trust/grant`       |
| Pause counterparty trust          | `POST /v1/ledger/counterparties/{counterparty_id}/trust/pause`       |
| Restore counterparty trust        | `POST /v1/ledger/counterparties/{counterparty_id}/trust/restore`     |
| Acknowledge counterparty review   | `POST /v1/ledger/counterparties/{counterparty_id}/trust/acknowledge` |
| List invoices                     | `GET /v1/ledger/invoices`                                            |
| Invoice detail                    | `GET /v1/ledger/invoices/{invoice_id}`                               |
| List obligations                  | `GET /v1/ledger/obligations`                                         |
| Governed resolved views           | `GET /v1/ledger/{obligations,counterparties,accounts}/{id}/resolved` |
| List transactions                 | `GET /v1/ledger/transactions`                                        |
| Transaction detail                | `GET /v1/ledger/transactions/{transaction_id}`                       |
| Promote Raw → Ledger              | `POST /v1/ledger/normalize`                                          |
| Trigger reconciliation            | `POST /v1/ledger/reconcile`                                          |
| List reconciliation matches       | `GET /v1/ledger/reconciliation-matches`                              |

### List Transactions

```http
GET /v1/ledger/transactions?account_id=acct_ops&since=2026-01-01&until=2026-03-31&direction=outflow
Authorization: Bearer <token>
```

```json
{
  "transactions": [
    {
      "id": "tx_001",
      "account_id": "acct_ops",
      "external_transaction_id": "plaid_tx_abc",
      "amount": "-1234.56",
      "currency": "USD",
      "direction": "outflow",
      "transaction_date": "2026-01-15",
      "posted_date": "2026-01-16",
      "counterparty_id": "cp_aws",
      "category_id": "cat_cloud",
      "status": "posted",
      "description_normalized": "AWS - cloud services",
      "reconciliation_status": "matched",
      "source_ids": ["raw_8231"],
      "evidence_ids": ["rp_001"],
      "confidence": 0.98
    }
  ],
  "next_cursor": "..."
}
```

Filters: `account_id`, `counterparty_id`, `direction` (`inflow | outflow | transfer | adjustment`), `status` (`pending | posted | cleared | failed | reversed | disputed`), `since`, `until`, `limit` (default 100, max 1000), `cursor`. `amount` is a signed decimal string.

### Get a Single Transaction

```http
GET /v1/ledger/transactions/{transaction_id}
Authorization: Bearer <token>
```

Returns the same `Transaction` shape. `404` if unknown.

### List Accounts

```http
GET /v1/ledger/accounts?status=active&account_type=bank_checking&limit=50
Authorization: Bearer <token>
```

```json
{
  "accounts": [
    {
      "id": "acct_ops",
      "owner_id": "acme",
      "account_type": "bank_checking",
      "name": "Operating",
      "currency": "USD",
      "status": "active",
      "institution": "Mercury",
      "external_account_id": "plaid_acc_xyz",
      "current_balance": "182431.45",
      "available_balance": "181009.12"
    }
  ],
  "next_cursor": null
}
```

`account_type` enum: `bank_checking | bank_savings | card | loan | line_of_credit | onchain | payment_processor`. Filters: `status` (`active | closed | frozen | pending`), `account_type`, `limit` (default 50, max 500), `cursor`.

For one account plus its latest balance:

```http
GET /v1/ledger/accounts/{account_id}
Authorization: Bearer <token>
```

```json
{ "account": { ... }, "latest_balance": { "current_balance": "182431.45", "as_of": "2026-05-28T11:55:00Z", "currency": "USD" } }
```

### List Balances (Point-in-Time)

```http
GET /v1/ledger/balances?account_id=acct_ops&as_of=2026-03-31T23:59:59Z
Authorization: Bearer <token>
```

Returns the balance row(s) effective at `as_of` (or the latest if omitted).

### List Counterparties

```http
GET /v1/ledger/counterparties?q=AWS&type=vendor&verified_status=document_verified
Authorization: Bearer <token>
```

`type` enum: `merchant | vendor | customer | employer | employee | bank | wallet | exchange | tax_authority | agent | other`. `verified_status`: `unverified | self_attested | document_verified | sanctions_cleared`. `trust_status`: `unreviewed | trusted | paused | acknowledged`. Each counterparty carries `risk_level` (`low | medium | high | sanctioned`), `aliases[]`, and `linked_accounts[]`. Filter list results with `trust_status` when needed.

### Create or Update a Counterparty

Create a manual counterparty with `ledger:write`:

```http
POST /v1/ledger/counterparties
Authorization: Bearer <user token>
Content-Type: application/json

{ "name": "Example Vendor", "type": "vendor", "contact_email": "ap@example.test" }
```

`name` and `type` are required. Optional identity fields are `display_name`, `category`, `contact_email`, `country`, `tax_id`, and `aliases`. The route returns `201` for a new record or `200` when its idempotent create result already exists.

Update editable identity fields with a user principal and `ledger:write`:

```http
PATCH /v1/ledger/counterparties/{counterparty_id}
Authorization: Bearer <user token>
Content-Type: application/json

{ "display_name": "Example Vendor Ltd." }
```

The patch cannot modify payment or trust fields. It returns `{ "counterparty": { ... } }`.

### Counterparty Trust

Trust transitions apply uniformly to every counterparty type, including vendors and customers. Each route requires a user bearer JWT with `ledger:write`. Platform shared-secret callers and API keys cannot use these routes.

| Transition  | Endpoint                                                             | Allowed prior state                     | Result         |
| ----------- | -------------------------------------------------------------------- | --------------------------------------- | -------------- |
| Grant       | `POST /v1/ledger/counterparties/{counterparty_id}/trust/grant`       | `unreviewed`, `acknowledged`            | `trusted`      |
| Pause       | `POST /v1/ledger/counterparties/{counterparty_id}/trust/pause`       | `unreviewed`, `trusted`, `acknowledged` | `paused`       |
| Restore     | `POST /v1/ledger/counterparties/{counterparty_id}/trust/restore`     | `paused`                                | `trusted`      |
| Acknowledge | `POST /v1/ledger/counterparties/{counterparty_id}/trust/acknowledge` | `unreviewed`, `paused`                  | `acknowledged` |

Every transition accepts an optional audit-only reason:

```json
{ "reason": "Reviewed supporting documents" }
```

Invalid transitions return `409` with `ledger_status_invalid`. Successful responses return the updated `counterparty` and `previous_trust_status`.

Trust state is informational today: payment execution is enforced through `verified_status`, sanctions screening, and policy checks. Trust state does not gate payment execution.

### List Invoices and Obligations

```http
GET /v1/ledger/invoices?status=sent
GET /v1/ledger/obligations?direction=receivable&status=due&due_before=2026-06-30
GET /v1/ledger/obligations?scenario=ar
```

`Invoice.status`: `draft | sent | partial | paid | overdue | cancelled | disputed`. `Obligation.type`: `bill | invoice | subscription | loan | rent | payroll | tax | card_statement | other`.

Both endpoints use opaque cursor pagination. Pass `limit` (default `50`, max `500`) and the preceding response's `next_cursor` as `cursor`; a final page returns `next_cursor: null`. Use `direction=receivable` on obligations when listing receivable obligations, or `scenario=ar` to select explicitly AR-marked rows. AR-sourced invoice and receivable-obligation rows carry `metadata.scenario: "ar"`. Clients should use that positive marker for classification rather than treating every non-AP row as receivable.

`GET /v1/ledger/invoices` is the complete Ledger source for a receivables inventory. `GET /v1/ledger/obligations?direction=receivable` contains only receivables that have an obligation projection, so it is not an interchangeably complete AR list.

Invoices can include `payment_instruction` when bank details were extracted from invoice evidence. The object contains display-safe `bank_name`, `routing_last4`, `swift_last4`, `account_last4`, the comparison status against the latest on-file counterparty payment instruction, and the compared history row id. Full routing, SWIFT, and account values are stored only as hashes and are never returned.

Fetch an individual invoice with:

```http
GET /v1/ledger/invoices/{invoice_id}
Authorization: Bearer <token>
```

It returns the tenant-scoped invoice row or `404`.

### Governed Resolved Views

The resolved views retain all source observations and present the current governed value, field-level authority, and conflicts for one record. Each route requires `ledger:read` and returns `404` when the record is absent or belongs to another tenant.

| Record       | Endpoint                                                   |
| ------------ | ---------------------------------------------------------- |
| Obligation   | `GET /v1/ledger/obligations/{obligation_id}/resolved`      |
| Counterparty | `GET /v1/ledger/counterparties/{counterparty_id}/resolved` |
| Account      | `GET /v1/ledger/accounts/{account_id}/resolved`            |

### Promote Raw → Ledger

Normalize a Raw-parsed row into typed Ledger entities. Idempotent. Re-running with the same input returns the same Ledger row ids.

```http
POST /v1/ledger/normalize
Authorization: Bearer <token>
Content-Type: application/json

{
  "raw_parsed_id":  "rp_001"
}
```

```json
{
  "ledger_rows_created": [
    { "entity": "transaction", "id": "tx_001" },
    { "entity": "counterparty", "id": "cp_aws" }
  ]
}
```

Normalization derives every entity implied by the parsed row. The route accepts an optional `target_entities` field for forward compatibility, but the current handler ignores it: it reads only `raw_parsed_id` and produces the full entity set the parsed row supports.

### Reconciliation

Trigger an async reconciliation pass:

```http
POST /v1/ledger/reconcile
Authorization: Bearer <token>
Content-Type: application/json

{ "since": "2026-03-01", "match_types": ["transaction_receipt", "invoice_payment"] }
```

`202 Accepted` → `{ "job_id": "rec_4711" }`.

List matches:

```http
GET /v1/ledger/reconciliation-matches?status=matched&match_type=invoice_payment
Authorization: Bearer <token>
```

```json
{
  "matches": [
    {
      "id": "rm_001",
      "match_type": "invoice_payment",
      "left_entity_type": "invoice",
      "left_entity_id": "inv_8231",
      "right_entity_type": "transaction",
      "right_entity_id": "tx_001",
      "confidence_score": 0.97,
      "status": "matched",
      "evidence_ids": ["rp_001"],
      "explanation": "amount + counterparty + date within tolerance"
    }
  ]
}
```

`match_type` enum: `transaction_receipt | invoice_payment | statement_balance | wallet_transfer | payroll_bank_debit | subscription_charge | card_charge | onchain_settlement | obligation_duplicate`. `status`: `unmatched | matched | partially_matched | duplicate_possible | disputed | cleared | failed | reversed`.

### Provenance on Every Row

Every Ledger row carries:

\| Field | Description | | --------------------------- | ------------------------------------------ | -------- | --------- | --------------- | ------------------ | | `source_ids` | Raw artifact ids that produced it | | `evidence_ids` | Raw-parsed row ids the extractor consulted | | `provenance` | `extracted | inferred | ambiguous | human_confirmed | agent_contributed` | | `confidence` | Calibrated 0 to 1 score | | `created_at` / `updated_at` | Bitemporal timestamps |

{% hint style="info" %}
Records are immutable. Corrections are written as **superseding** records that reference what they correct (`supersedes` field). The history is preserved end-to-end. Query `GET /v1/audit/entity/{entityType}/{entityId}` for the full causal trail.
{% endhint %}

### What's Next

<table data-view="cards"><thead><tr><th></th><th></th><th data-type="content-ref"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>🧠 Wiki API</strong></td><td>Reason over the Ledger in natural language.</td><td><a href="/platform/wiki-api.md">Wiki API</a></td><td></td></tr><tr><td><strong>📥 Raw and Ledger</strong></td><td>The conceptual model.</td><td><a href="/platform/raw-and-ledger.md">Raw and Ledger</a></td><td></td></tr></tbody></table>
