Quick Start
From nothing to a posted, balanced transaction.
Step 1: Get credentials
Settings → API Keys. You need both the key and the secret — they go in two
separate headers, not an Authorization header.
export LEDGER_URL=https://ledger.finopsbricks.com
export KEY=your_key
export SECRET=your_secret
Step 2: Look at the chart of accounts
Every organization starts with a skeleton of system group accounts. Nothing can post to those — you post to accounts you hang beneath them.
curl -s "$LEDGER_URL/api/v1/accounts/tree" \
-H "api-key: $KEY" -H "api-secret: $SECRET"
Step 3: Create two postable accounts
# Find the id of the seeded CASH group
CASH=$(curl -s "$LEDGER_URL/api/v1/accounts?source=system&source_ref=CASH" \
-H "api-key: $KEY" -H "api-secret: $SECRET" | jq -r '.data[0].id')
# A bank account beneath it
curl -s -X POST "$LEDGER_URL/api/v1/accounts" \
-H "api-key: $KEY" -H "api-secret: $SECRET" \
-H "content-type: application/json" \
-d "{\"name\": \"HDFC Current A/c\", \"type\": \"asset\", \"parent\": \"$CASH\"}"
# An expense account
curl -s -X POST "$LEDGER_URL/api/v1/accounts" \
-H "api-key: $KEY" -H "api-secret: $SECRET" \
-H "content-type: application/json" \
-d '{"name": "Rent Expense", "type": "expense"}'
A parent must be a group of the same type — an expense account cannot hang under an asset group, because a group's total is the sum of what sits beneath it.
Step 4: Post a transaction
Amounts are integers in minor units. 5000000 is ₹50,000.00.
curl -s -X POST "$LEDGER_URL/api/v1/transactions" \
-H "api-key: $KEY" -H "api-secret: $SECRET" \
-H "content-type: application/json" \
-d '{
"date": "2026-08-05",
"narration": "Rent paid — Aug 2026",
"type": "vendor_paid",
"entries": [
{ "account": "RENT_ACCOUNT_ID", "debit": 5000000, "credit": 0 },
{ "account": "BANK_ACCOUNT_ID", "debit": 0, "credit": 5000000 }
]
}'
Debits must equal credits. If they do not, the response tells you by how much:
{
"error": {
"code": "UNBALANCED",
"message": "Debits and credits must be equal. Debits total 5000000, credits total 4500000, a difference of 500000.",
"details": { "total_debit": 5000000, "total_credit": 4500000, "difference": 500000 }
}
}
The difference is usually the line you left out.
Step 5: Read it back
# The transaction with its entries
curl -s "$LEDGER_URL/api/v1/transactions?date_from=2026-08-01&date_to=2026-08-31" \
-H "api-key: $KEY" -H "api-secret: $SECRET"
# Just the lines that hit one account
curl -s "$LEDGER_URL/api/v1/entries?account=BANK_ACCOUNT_ID" \
-H "api-key: $KEY" -H "api-secret: $SECRET"
If you are importing from another system
Send source and source_ref on everything:
{ "source": "sap", "source_ref": "4900001234", "...": "..." }
Together they are the row's identity. Re-sending returns the existing row with
created: false instead of duplicating it, so an interrupted sync can simply be
re-run. Without them, a retry doubles your ledger.
If you are mirroring two systems at once
A granular subsystem feeding a summarised one — millions of charging sessions
invoiced by one service, one summary document per state per month reaching SAP —
can be held here at both levels. source keeps the namespaces apart.
The trap is double-counting, and the fix is to post the two layers against different accounts, with the summary layer hitting a clearing account. That account must return to zero at every summary posting date. When it does not, the two layers disagree and something upstream went wrong — the ledger becomes a reconciliation point rather than a place where the error hides.
Drafts
Post directly unless you need to build a transaction up over several calls.
{ "status": "draft", "...": "..." }
A draft may be unbalanced and is excluded from reports. Post it with
PATCH /api/v1/transactions/:id and {"status": "posted"} — the balance check
runs then.
Once posted, a transaction is immutable. Correct one by voiding it and posting a replacement.