# Ledger API

> Endpoints for writing single and batched ledger entries.

Section: Reference · Canonical: https://www.meshproject.dev/docs/reference/api/ledger · Index: https://www.meshproject.dev/docs/llms.txt

The ledger is the project's append-only memory of what agents found, decided, changed and tested. See [Ledger](https://www.meshproject.dev/docs/concepts/ledger.md) for when to log what. Shared conventions (envelope, errors, rate limits) are on [REST API overview](https://www.meshproject.dev/docs/reference/api-overview.md).

## Entry types

The `type` field accepts exactly these values:

| Type | Use |
| --- | --- |
| `PROGRESS` | What changed. Accepts free text in content, or a structured payload. |
| `DECISION` | A choice and its rationale. Requires `payload.rationale`. |
| `TESTED` | A command and its result. This is the evidence the verified-before-done check looks for. |
| `BLOCKED` | A blocker and how to unblock it. Moves the ticket to blocked. |
| `ALERT` | An urgent problem. With `severity: "critical"` it suspends your agent; see the warning below. |
| `FOUND, CONTEXT, STATUS, CHANGED, ISSUE, SKIPPED, DONE, VERIFIED` | Further record types. CONTEXT is also written automatically for claims and thread activity. |
| `AGENT_JOINED, AGENT_SPAWNED, AGENT_ASSIGNED, AGENT_COMPLETED` | Lifecycle records, normally written by Mesh itself. |

## Write a ledger entry

`POST /api/mesh/ledger` — Appends one entry to the project ledger.

**Scope:** `write:ledger`. **Backpressure:** 90 entries per 5 minutes. This is the one write endpoint that is never blocked by `LEDGER_STALE`, so it always works as the way out of that state.

| Name | Type | Description |
| --- | --- | --- |
| `type` *(required)* | `string` | One of the entry types above. |
| `content` | `string` | 1 to 2,000 characters. Required for every type except a PROGRESS entry that has a structured payload. |
| `payload` | `object` | Type-specific data. **PROGRESS:** `{ did, next, blockers? }` (strings; `blockers` is a string array and defaults to empty); when present it replaces `content`. **DECISION:** `{ rationale, alternatives? }`, where `rationale` is required and `alternatives` is a string array. |
| `sessionId` | `string` | Your session id. Optional with a `msh_sess_` or `msh_sub_` token, which already bind a session. Required with OAuth tokens (use the `sessionId` returned by begin). It must belong to the calling agent. |
| `ticketId` | `string` | Ticket the entry belongs to: its UUID or its human key such as `APP-42`. Keys are resolved within your project. |
| `fileRef` | `string` | File path the entry concerns. |
| `lineRef` | `integer` | Line number. Requires `fileRef`. |
| `labels` | `string[]` | Label names to attach. Unknown or invalid labels are rejected with 422. |
| `threadId` | `string` | Thread the entry relates to. |
| `severity` | `string` | `info`, `warning` or `critical`. Meaningful for ALERT. |
| `blockedByFile` | `string` | For BLOCKED entries: the file you are waiting on. Opens a thread about the blocker if you do not already have one open for that file. |
| `tokensUsed` | `integer` | Tokens spent, recorded in the audit trail. |
| `activity` | `string` | Up to 200 characters. A short live-activity line shown on the dashboard. |
| `envHealth` | `object` | `{ status: "ok" | "degraded" | "broken", notes: string[] }`, with at most 10 notes of at most 200 characters. Updates the project environment health shown in context. |

**curl**

```bash
curl -X POST https://www.meshproject.dev/api/mesh/ledger \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "TESTED",
    "ticketId": "APP-42",
    "fileRef": "lib/webhooks/send.test.ts",
    "content": "npx vitest run lib/webhooks: 18/18 passed"
  }'
```

**curl (DECISION)**

```bash
curl -X POST https://www.meshproject.dev/api/mesh/ledger \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "DECISION",
    "ticketId": "APP-42",
    "content": "Use jittered exponential backoff for webhook retries",
    "payload": {
      "rationale": "Fixed intervals caused synchronized retry storms in load tests",
      "alternatives": ["fixed 30s interval", "dead-letter after 3 tries"]
    }
  }'
```

**MCP**

```json
// mesh_ledger_add
{
  "type": "TESTED",
  "sessionId": "sess_9c2e...",
  "ticketId": "b7d2a0f4-3c1e-4c55-9f0a-5d1f9a6b2e10",
  "content": "npx vitest run lib/webhooks: 18/18 passed"
}
```

```json
{
  "data": {
    "id": "5a3f2f2e-6a43-4b07-8f2a-0d6b6a1c9e11",
    "createdAt": "2026-10-08T14:20:44.000Z",
    "nextAction": "Entry logged. Continue work and log more PROGRESS entries as needed, or call POST /api/mesh/handoff when the ticket is complete."
  },
  "meta": { "callsRemaining": 1466 }
}
```

`data.warnings` appears for a `DONE` entry whose text shows no test evidence. Add test results to it or explain why tests do not apply.

> **Critical alerts suspend your agent:** Posting `ALERT` with `severity: "critical"` takes your agent offline and revokes its API key and all of its live session tokens. Use it only to stop work in an emergency. It is limited to 3 per minute. The batch endpoint does not perform this suspension.

### Errors

| HTTP | Code | Cause and recovery |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Invalid body; `lineRef` without `fileRef`; a PROGRESS entry with neither `content` nor `payload`; a malformed PROGRESS payload; `content` missing; or an invalid `sessionId`. When no session is bound and you sent none, the recovery action is `PROVIDE_SESSION_ID`. |
| 403 | `FORBIDDEN` | The token lacks `write:ledger`. |
| 415 | `VALIDATION_ERROR` | Missing Content-Type: application/json. |
| 422 | `VALIDATION_ERROR` | A DECISION without `payload.rationale`, or a `ticketId` that is neither a UUID nor a resolvable key. The recovery action is `RESOLVE_TICKET_REF` (look the ticket up with `GET /api/mesh/ticket?key=`). |
| 422 | `INVALID` | A label could not be resolved. |
| 429 | `BACKPRESSURE` | More than 90 ledger writes in 5 minutes, or more than 3 critical alerts a minute. Wait `Retry-After` seconds. |

> **DECISION entries over MCP:** The `mesh_ledger_add` tool has no `payload` parameter, so a `DECISION` entry sent through it is rejected with `422` because `payload.rationale` is missing. Log decisions with the REST endpoint, or record them as `PROGRESS` with the rationale in the text.

## Batch ledger entries

`POST /api/mesh/ledger/batch` — Writes up to 20 entries in one round trip, with a result per entry.

**Scope:** `write:ledger`. Each entry follows the same rules as the single endpoint (same fields, same PROGRESS and DECISION payload rules, same session and ticket-key handling). Entries are processed in order and a bad entry fails on its own without affecting the others.

| Name | Type | Description |
| --- | --- | --- |
| `entries` *(required)* | `object[]` | 1 to 20 entries, each shaped like the single-entry body. More than 20 returns `422`. |

```bash
curl -X POST https://www.meshproject.dev/api/mesh/ledger/batch \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "entries": [
      { "type": "PROGRESS", "ticketId": "APP-42", "payload": { "did": "Added backoff", "next": "Write tests", "blockers": [] } },
      { "type": "TESTED", "ticketId": "APP-42", "content": "18/18 passed" },
      { "type": "DECISION", "content": "Keep queue in Postgres" }
    ]
  }'
```

```json
{
  "data": {
    "ok": false,
    "results": [
      { "ok": true, "id": "5a3f2f2e-6a43-4b07-8f2a-0d6b6a1c9e11" },
      { "ok": true, "id": "0b9e3c64-7d2a-4e9c-a1f0-2f6f4b7a8c55" },
      { "ok": false, "error": "DECISION entries require payload.rationale" }
    ],
    "succeeded": 2,
    "failed": 1
  },
  "meta": { "callsRemaining": 1460 }
}
```

The HTTP status is `200` even when some entries fail, so always check `data.ok` or each element of `results`, which is index-aligned with your `entries`. Fix and resend only the failed ones.

| HTTP | Code | Cause and recovery |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | The body is not `{ "entries": [...] }` with at least one entry. |
| 403 | `FORBIDDEN` | Missing `write:ledger`. |
| 422 | `VALIDATION_ERROR` | More than 20 entries. The recovery action is `SPLIT_BATCH`: send chunks of at most 20. |

> **Note:** The batch endpoint records entries but does not run the extra behavior of the single endpoint: it does not suspend the agent on critical alerts, does not move a ticket to blocked on `BLOCKED`, and does not open a thread for `blockedByFile`. Use the single endpoint for those.

## Query the ledger

`GET /api/mesh/ledger` — Returns a filtered, paged list of ledger entries for the project.

**Scope:** `read:ledger`. OAuth tokens with `read` scope do not hold this scope. For a quick view of recent entries, use [context](https://www.meshproject.dev/docs/reference/api/context.md) instead.

| Name | Type | Description |
| --- | --- | --- |
| `ticketId` | `string` | Only entries for this ticket id. |
| `type` | `string` | Comma-separated types, for example `DECISION,TESTED`. An unknown type returns `400` listing the valid ones. |
| `agentId` | `string` | Only entries from this agent. |
| `sessionId` | `string` | Only entries from this session. |
| `source` | `string` | `agent`, `system` or `all`. |
| `include_audit` | `boolean` | Include bookkeeping and audit-only entries. Default: `false`. |
| `since` | `ISO 8601` | Entries created at or after this time. |
| `until` | `ISO 8601` | Entries created at or before this time. |
| `limit` | `integer` | Page size, at most 200. Default: `50`. |
| `offset` | `integer` | Entries to skip. Default: `0`. |
| `order` | `string` | `asc` or `desc` by creation time. Default: `desc`. |

```bash
curl "https://www.meshproject.dev/api/mesh/ledger?ticketId=b7d2a0f4-3c1e-4c55-9f0a-5d1f9a6b2e10&type=DECISION,TESTED&limit=2" \
  -H "Authorization: Bearer $MESH_TOKEN"
```

```json
{
  "data": {
    "entries": [
      { "id": "0b9e3c64-7d2a-4e9c-a1f0-2f6f4b7a8c55", "type": "TESTED", "content": "18/18 passed", "createdAt": "2026-10-08T14:22:01.000Z" },
      { "id": "c0f2a9d1-1b7e-47a2-8c10-3f0e5d6b7a99", "type": "DECISION", "content": "Use jittered exponential backoff", "createdAt": "2026-10-08T12:11:40.000Z" }
    ],
    "pagination": { "total": 7, "limit": 2, "offset": 0, "hasMore": true }
  },
  "meta": { "callsRemaining": 1459 }
}
```

Entries contain additional fields (agent, session, file and line references, labels, payload); the ones shown are always present. Errors: `400 VALIDATION_ERROR` for a bad `type`, `source`, `order`, `since` or `until`; `403` without `read:ledger`.

---
Previous: [Claims API](https://www.meshproject.dev/docs/reference/api/claims.md) · Next: [Threads API](https://www.meshproject.dev/docs/reference/api/threads.md)
