Install
The CLI ships the same client the SDKs wrap. Node 18+ or any runtime with fetch.
npm i @ledgerdoc/clientLedgerdoc is a double-entry ledger API. Every posting is immutable, every write is idempotent, and every balance is derived from the entries rather than stored beside them — so the books cannot drift out of agreement with their own history.
Built for teams who moved money on a spreadsheet for too long. Post your first balanced entry in about four minutes, then keep the same two endpoints in production.
# A balanced double-entry posting.
# Debits and credits must sum to zero.
curl -X POST https://api.ledgerdoc.dev/v2/entries \
-H "Authorization: Bearer $LEDGER_KEY" \
-H "Idempotency-Key: inv-4417-settle" \
-d '{
"narrative": "Invoice 4417 settled",
"lines": [
{ "account": "cash:operating", "amount": 48200 },
{ "account": "ar:acme-holdings", "amount": -48200 }
]
}'
# 201 Created
# { "id": "ent_8Fq2wZ", "balanced": true, "posted_at": ... }
Copy these in order. The sandbox needs no card and no approval — keys are issued the moment you create a workspace.
The CLI ships the same client the SDKs wrap. Node 18+ or any runtime with fetch.
npm i @ledgerdoc/clientSandbox keys start sk_test_. They never touch live balances.
export LEDGER_KEY=sk_test_9f2aLines must sum to zero. An unbalanced entry is rejected, never partially written.
ledger entries create \
--from cash:operating \
--to ar:acme 48200Left nav, article body, and an "On this page" rail that tracks your scroll. This is a real article from the guide, not a placeholder — twenty-one more are indexed by topic.
How Ledgerdoc guarantees that a retried write posts exactly once, even when the network disagrees about whether the first attempt arrived.
Every write endpoint accepts an Idempotency-Key header. The key is an arbitrary string you choose, scoped to your workspace, and retained for 24 hours. If two requests arrive carrying the same key, the second returns the original response — it does not create a second entry.
A client that times out cannot tell the difference between a request that never arrived and a response that was lost on the way back. Retrying is the only safe behaviour, so the ledger has to make retrying safe.
Generate the key from something stable in your own domain — an invoice ID, a payout batch, a settlement reference. Never generate it per attempt, or every retry becomes a new posting.
The response to a replayed request carries a Replayed: true header so you can distinguish it in logs. The body is byte-identical to the first response, including the original posted_at timestamp.
# Same Idempotency-Key, sent twice.
HTTP/1.1 201 Created
Replayed: true
Content-Type: application/json
{
"id": "ent_8Fq2wZ",
"balanced": true,
"posted_at": "2026-07-14T09:12:44Z"
}
Reusing a key with a different payload is an error, not a silent overwrite. The ledger returns 409 idempotency_conflict and writes nothing.
| Condition | Status | Result |
|---|---|---|
| Same key, same payload | 201 | Original response replayed, no new entry |
| Same key, changed payload | 409 | Rejected as idempotency_conflict |
| Same key, after 24h | 201 | Treated as new — key has expired |
| No key supplied | 201 | Posted, but retries are unsafe |
Keys are scoped per workspace and per environment. A sandbox key and a live key never collide. Retention is 24 hours from first use; after that the key is forgotten and a request carrying it is treated as new.
Every route is versioned under /v2. Scopes are checked per key; a read key cannot post.
| Method | Path | Description | Scope |
|---|---|---|---|
| POST | /v2/entries | Post a balanced double-entry transaction | entries:write |
| GET | /v2/entries/:id | Retrieve a single posting with its lines | entries:read |
| GET | /v2/entries | List postings, filtered by account or window | entries:read |
| POST | /v2/accounts | Create an account in the chart | accounts:write |
| GET | /v2/accounts/:id/balance | Derive a balance at an optional timestamp | accounts:read |
| PATCH | /v2/accounts/:id | Rename or re-tag an account, never its history | accounts:write |
| POST | /v2/reconciliations | Open a reconciliation window against a statement | recon:write |
| DELETE | /v2/webhooks/:id | Remove a webhook subscription | hooks:write |
Postings are immutable. There is deliberately no PATCH or DELETE on /v2/entries — corrections are made by posting a reversing entry.
Every SDK is generated from the same schema, so field names and error codes match the reference exactly. Six shown; twelve published.
npm i @ledgerdoc/clientpip install ledgerdocgo get ledgerdoc.dev/gogem install ledgerdoccargo add ledgerdocmix deps.get ledgerdocBreaking changes ship only on a major version and are announced 90 days ahead. Everything else lands continuously.
Replayed: true header for log correlation.posted_at instead of the original.GET /v2/accounts/:id/balance accepts an at timestamp to derive historical balances./v2/hooks alias is removed. Migrate to /v2/webhooks.GET /v2/entries is roughly 4× faster on large accounts.PATCH /v2/entries/:id is removed. Post a reversing entry instead.Six most recent shown. All fourteen releases, filterable by version →
Public, searchable and answered by the engineers who wrote the endpoints. Median first reply is under four hours on weekdays.
Open call every Thursday at 16:00 UTC. Bring a failing request and we will read the trace with you. No agenda, no slides.
Standard is included. Priority adds a 1-hour response target and a named engineer; Platform adds a shared incident channel and design review.
600 writes and 3,000 reads per minute per workspace on the default plan, burstable to double for 30 seconds. Replayed idempotent requests are not counted. Every response carries X-RateLimit-Remaining; a 429 includes Retry-After in seconds.
Yes. Sandbox keys begin sk_test_ and operate on isolated data with the same validation rules and the same latency profile. Sandbox data is reset every 30 days and is never billed.
The version is in the path. A major version is supported for 24 months after its successor ships, and breaking changes are announced 90 days ahead on the changelog and by email to workspace owners. Minor releases are additive and never remove a field.
Workspaces are pinned to one region at creation — Dublin, Frankfurt, Virginia or Sydney — and entries never leave it. Backups stay in-region. Region cannot be changed after creation; create a new workspace and replay your entries instead.
Yes, at any time and without asking us. GET /v2/entries with a cursor walks the full history as newline-delimited JSON, and the CLI wraps it as ledger export --all. There is no proprietary format and no export fee.
Nothing is edited. Post a reversing entry that references the original, then post the correction. The audit chain stays intact and the balance derives correctly from the three entries together — which is what an auditor expects to see.
Create a workspace, take a sandbox key, and post a balanced entry before your coffee goes cold. No card, no sales call.
Perfect for: platform teams replacing a spreadsheet ledger with something auditable