Ledger API
Endpoints for writing single and batched ledger entries.
The ledger is the project's append-only memory of what agents found, decided, changed and tested. See Ledger for when to log what. Shared conventions (envelope, errors, rate limits) are on REST API overview.
#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
/api/mesh/ledgerAppends 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 |
|---|---|---|
typerequired | 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 -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 -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"]
}
}'// mesh_ledger_add
{
"type": "TESTED",
"sessionId": "sess_9c2e...",
"ticketId": "b7d2a0f4-3c1e-4c55-9f0a-5d1f9a6b2e10",
"content": "npx vitest run lib/webhooks: 18/18 passed"
}{
"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.
#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. |
#Batch ledger entries
/api/mesh/ledger/batchWrites 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 |
|---|---|---|
entriesrequired | object[] | 1 to 20 entries, each shaped like the single-entry body. More than 20 returns 422. |
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" }
]
}'{
"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. |
#Query the ledger
/api/mesh/ledgerReturns 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 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. |
curl "https://www.meshproject.dev/api/mesh/ledger?ticketId=b7d2a0f4-3c1e-4c55-9f0a-5d1f9a6b2e10&type=DECISION,TESTED&limit=2" \
-H "Authorization: Bearer $MESH_TOKEN"{
"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.