An API for bank statements, invoices, and receipts.
Send a file, get typed rows back. The open-source TypeScript SDK does the hashing, the upload, and the polling in one call, and the API underneath is on every plan.
- 0
- Runtime dependencies
- 6
- Document types
- 8
- Signed event types
npm install @ledgerbox/sdk import { LedgerBox } from "@ledgerbox/sdk"
import { extractFile } from "@ledgerbox/sdk/node"
const lb = new LedgerBox({ apiKey, organizationId, workspaceId })
const { records } = await extractFile(lb, "statement.pdf") The five steps the SDK takes for you
Nothing you could not do yourself. The order is the reason a single curl cannot upload a document: the second request needs a URL out of the first, and the file needs a hash before either.
- Hashes the file SHA-256, so the upload is verifiable.
- Creates a batch POST /v1/batches, with an idempotency key so a retry cannot double-charge you.
- Uploads the bytes Straight to storage on the short-lived URL the API returns. The key never travels with them.
- Completes the upload POST to the completion route, which enqueues extraction and returns a document id.
- Waits for the result Polls with backoff until the document stops processing, then reads the rows back.
Every step is a method you can call on its own when you want the pieces.
The same call with curl
KEY="lb_..." # Settings, API keys
ORG="..." # shown beside the key
WORKSPACE="..." # shown beside the key
curl "https://api.ledgerbox.io/v1/documents?limit=1" \
-H "x-api-key: $KEY" \
-H "x-organization-id: $ORG" \
-H "x-workspace-id: $WORKSPACE" Events you can subscribe to
One endpoint can take all of them or a subset. Webhook endpoints need Pro and up; polling the API works on every plan.
Document lifecycle
- document_processed Extraction finished and results are ready to read.
- flags_created Extraction raised review flags on a document.
- document_approved A reviewer approved the extracted data.
- export_generated An export file was created for a document.
- posting_completed A document was posted to an accounting destination.
- batch_completed Every document in one upload finished processing.
Housekeeping
- artifact_expired A stored file passed its retention window.
- purge_completed A requested deletion finished purging its files.
How deliveries are signed and retried
Every delivery is a POST with a JSON body, and four of its headers are ours. Verify the signature before you parse the body: an unsigned or badly signed request is not from us.
An example delivery
POST /hooks/ledgerbox HTTP/1.1
content-type: application/json
user-agent: LedgerBox-Webhooks/1.0
ledgerbox-event: document_processed
ledgerbox-delivery: f0c2a739-8e14-4b5d-9036-2a7c8e1f4b60
ledgerbox-timestamp: 1774343643
ledgerbox-signature: v1=4e1b9c...
{
"id": "9c1f4a2e-6b70-4c5a-9f83-1d0e2b7a45c1",
"deliveryId": "f0c2a739-8e14-4b5d-9036-2a7c8e1f4b60",
"type": "document_processed",
"createdAt": "2026-07-24T09:14:03.271Z",
"data": {
"eventId": "9c1f4a2e-6b70-4c5a-9f83-1d0e2b7a45c1",
"eventType": "document_processed",
"documentId": "3f6b21d8-9a54-4e07-b1c2-8d5f7e04a9b3",
"processingRunId": "c4d8e5f1-2a37-4b96-8c0d-5e1f3a7b2d64",
"state": "succeeded",
"derivedState": "needs_review"
}
} - ledgerbox-event
- The event type. A test send arrives as test.
- ledgerbox-delivery
- The delivery id, stable across retries of the same event. Treat it as an opaque string.
- ledgerbox-timestamp
- Unix seconds, regenerated on every attempt.
- ledgerbox-signature
- v1= followed by the hex digest.
How the signature is built
v1= followed by the hex HMAC-SHA256 of the timestamp, a full stop, and the raw body, keyed with the endpoint's signing secret. Compare in constant time and reject anything older than a few minutes.
Verify the signature
import { createHmac, timingSafeEqual } from "node:crypto"
export function verifyLedgerBox(rawBody, headers, secret) {
const timestamp = headers["ledgerbox-timestamp"]
const signature = headers["ledgerbox-signature"]
if (!timestamp || !signature) return false
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const expected =
"v1=" +
createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(signature)
return a.length === b.length && timingSafeEqual(a, b)
} When your endpoint is down
A delivery is attempted up to eight times in all, with the gap growing from 30 seconds through minutes to a last wait of 12 hours. The delivery id is stable across attempts, so deduplicating on it is enough. An endpoint can be paused instead of deleted while you fix it.
The full reference
Every endpoint, every parameter, and the response shapes, generated from the API itself.
Common questions
Which plan does the API need?
Calling the API works on every plan, and pages are metered the same way whether a document arrives through the API or the dashboard. Webhooks are separate and need Pro and up. The free trial includes both, so you can build against them before paying. A key without an active subscription behind it is refused with subscription_required rather than returning empty results.
Is the SDK really open source?
Yes. @ledgerbox/sdk is Apache-2.0, and it declares no runtime dependencies at all, so installing it adds exactly one package to your tree and pulls nothing else onto your machine. It talks to the same public API documented here, so nothing it does is unavailable to you directly.
Are there rate limits?
Yes, per organization and per minute, counted separately for reads, writes, and exports. Trials get a smaller budget than paid plans. Every response carries ratelimit-limit, ratelimit-remaining, and ratelimit-reset, and a refusal is a 429 with Retry-After. The SDK retries those automatically with backoff, so a well-behaved integration does not usually notice them.
Where are API keys created?
Settings, API keys in the app. The key is shown once, at creation, along with the organization and workspace ids the requests need. Keys carry scopes: uploading needs upload and status, review actions need process, exports need export, and downloads need download.
How do I verify a webhook signature?
Take the raw request body, exactly as received, and compute an HMAC-SHA256 over the ledgerbox-timestamp value, a full stop, and that body, keyed with the endpoint's signing secret. Compare it to the ledgerbox-signature header in constant time, and reject anything older than a few minutes.
What happens when my endpoint is down?
A delivery is attempted up to eight times in all, with the gap growing from 30 seconds to 2 minutes, 10 minutes, 30 minutes, and then 2, 6, and 12 hours. The delivery id stays the same across every attempt, so you can deduplicate on it. An endpoint can also be paused rather than deleted while you fix it.
Can events be replayed?
Yes. Retries are automatic while an endpoint is failing, and any delivery in the log can also be replayed by hand from Settings, Integrations. A replay is sent as a new delivery carrying the original event id, so a receiver that deduplicates on the envelope id will correctly ignore one it has already handled. If your receiver was down for longer than the retry window and there is more to catch up on than you want to replay, read the current state back through the API instead.
Wondering what the API returns for each type? See every document type Posting instead of polling? How posting works
6 document types, one endpoint.
Create a key and send your first document in a minute. No card required to start.