Accounting API for developers: the whole ledger over REST
Ledgeriano's accounting API gives your code everything the dashboard can do for one business: accounts, journal entries, fiscal years, parties, exchange rates, financial reports, inventory and cost accounting, budgets and management accounting, webhooks and the audit trail. JSON in, JSON out, with an OpenAPI reference you can try in the browser.

- JSON over HTTPS, OpenAPI documented
- REST
- owner-managed API keys
- Bearer
- default rate limit per key
- 120/min
- webhook event types
- 16
Base URL
https://api.ledgeriano.com/api/v1Every business has its own keys; the key decides which business you are talking to.
From zero to a posted journal entry in three steps
- 1
Create a key
As the business owner, open the business settings, create an API key and choose its abilities: read, or read and write. Copy it right away: the full key is shown only once.
- 2
Make a read call
Send
GET /accountswithAuthorization: Bearer lgr_live_…. The response lists the chart of accounts of that business, and the headers show your remaining rate limit and credit balance. - 3
Post an entry
Send
POST /journal-entrieswith balanced lines and anIdempotency-Key. Use account codes or ids, add"status": "posted"to post immediately, or leave it as a draft for review.
Post a journal entry in curl, JavaScript or Python
The same request in three languages: a posted sales entry with a customer on the receivable line. Retrying with the same idempotency key returns the original entry instead of creating a second one.
Read accounts and reports
# Trial balance for the current fiscal year
curl https://api.ledgeriano.com/api/v1/reports/trial-balance \
-H "Authorization: Bearer $ACCOUNTING_API_KEY" \
-H "Accept: application/json"const res = await fetch("https://api.ledgeriano.com/api/v1/accounts?postable=1", {
headers: {
Authorization: `Bearer ${process.env.ACCOUNTING_API_KEY}`,
Accept: "application/json",
},
});
const { data: accounts } = await res.json();
console.log(res.headers.get("X-RateLimit-Remaining"), accounts.length);import os
import requests
resp = requests.get(
"https://api.ledgeriano.com/api/v1/reports/balance-sheet",
params={"compare": 1}, # add the prior year column
headers={
"Authorization": f"Bearer {os.environ['ACCOUNTING_API_KEY']}",
"Accept": "application/json",
},
timeout=30,
)
report = resp.json()curl -X POST https://api.ledgeriano.com/api/v1/journal-entries \
-H "Authorization: Bearer $ACCOUNTING_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-10482" \
-d '{
"entry_date": "2026-09-25",
"voucher_type": "SAL",
"reference": "INV-10482",
"description": "Online order 10482",
"status": "posted",
"lines": [
{ "account_code": "1111", "debit": 1250.00, "party_code": "C001" },
{ "account_code": "4101", "credit": 1250.00 }
]
}'const res = await fetch("https://api.ledgeriano.com/api/v1/journal-entries", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ACCOUNTING_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
"Idempotency-Key": "order-10482", // same key on retry = same entry
},
body: JSON.stringify({
entry_date: "2026-09-25",
voucher_type: "SAL",
reference: "INV-10482",
description: "Online order 10482",
status: "posted",
lines: [
{ account_code: "1111", debit: 1250.0, party_code: "C001" },
{ account_code: "4101", credit: 1250.0 },
],
}),
});
if (!res.ok) {
const err = await res.json(); // { code, message, errors }
throw new Error(`${res.status} ${err.code}: ${err.message}`);
}
const { data: entry } = await res.json();
console.log(entry.number, res.headers.get("X-Credits-Balance"));import os
import requests
resp = requests.post(
"https://api.ledgeriano.com/api/v1/journal-entries",
headers={
"Authorization": f"Bearer {os.environ['ACCOUNTING_API_KEY']}",
"Accept": "application/json",
"Idempotency-Key": "order-10482",
},
json={
"entry_date": "2026-09-25",
"voucher_type": "SAL",
"reference": "INV-10482",
"description": "Online order 10482",
"status": "posted",
"lines": [
{"account_code": "1111", "debit": 1250.00, "party_code": "C001"},
{"account_code": "4101", "credit": 1250.00},
],
},
timeout=30,
)
if resp.status_code >= 400:
err = resp.json()
raise RuntimeError(f"{resp.status_code} {err['code']}: {err['message']}")
entry = resp.json()["data"]
print(entry["number"], resp.headers.get("X-Credits-Balance"))API keys that only the owner can create, rotate and revoke
Send the key as a bearer token: Authorization: Bearer lgr_live_…. The X-API-Key header works too. Keys belong to one business, so an integration for three companies uses three keys, and a leaked key never exposes the other books.
A key with the read ability can call read endpoints. Anything that changes data needs write; without it the API answers 403. Keys can have an expiry date, can be rotated (the old key stops working and a new one is issued with the same settings) and can be revoked at any time. Every call is logged with the key that made it.
Authorization: Bearer lgr_live_xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json
Accept-Language: en # or fa
Idempotency-Key: order-10482 # on createreadwriterotaterevokeexpires_atWhat the API covers
All paths are relative to the base URL. Lines in journal entries accept ids (account_id, party_id, cost_center_id, project_id) or codes (account_code, party_code and so on).
| Resource | Endpoints | Notes |
|---|---|---|
| Fiscal years | GET POST /fiscal-years, GET PUT DELETE /fiscal-years/{id}, POST /{id}/close, POST /{id}/reopen | Closing writes the closing and opening entries |
| Accounts | /accounts CRUD, GET /accounts/{id}/balance, PUT /accounts/cost-behavior | Chart of accounts of the current fiscal year; cost behaviour (fixed, variable, mixed) in bulk |
| Parties | /parties CRUD | Customers and suppliers |
| Cost centers | /cost-centers CRUD | Departments, branches |
| Projects | /projects CRUD | Jobs and contracts |
| Voucher types | /voucher-types CRUD | Codes such as SAL, PUR, PAY |
| Journal entries | /journal-entries CRUD, POST /bulk-post, POST /{id}/post, POST /{id}/reverse, POST /{id}/duplicate, /{id}/attachments | Drafts can be edited; posted entries are corrected by reversal |
| Exchange rates | GET POST /exchange-rates, DELETE /exchange-rates/{id} | Used when an entry has no rate of its own |
| Reports | /reports/trial-balance, balance-sheet, income-statement, cash-flow, equity-changes, general-ledger, journal, party-balances, dashboard | Same numbers as the dashboard |
| Inventory setup | GET POST DELETE /inventory/setup | Turns the inventory and costing module on or off and maps its accounts |
| Inventory master data | /units, /warehouses, /item-categories, /items CRUD | Valuation method per item: FIFO, weighted average, standard or specific identification |
| Lots and serials | /lots CRUD, /inventory-reports/lot-balances, expiring-lots, lot-trace, serial-history | FEFO allocation, backward and forward traceability |
| Stock documents | /stock-documents CRUD, POST /{id}/post, POST /{id}/cancel | Receipts, returns, issues, deliveries, transfers, adjustments, landed costs, write-downs |
| Stock counts | /stock-counts, POST /{id}/complete, POST /{id}/cancel | Completing a count posts the adjustment |
| Inventory reports | /inventory-reports/on-hand, kardex, movement-summary, reconciliation, ageing, reorder | Stock ledger reconciled with the general ledger |
| Production | /boms CRUD, POST /boms/{id}/roll-up, /cost-center-rates, /production-orders with release, issue, receive, close, reopen, GET /{id}/cost-sheet | Job order costing with standard cost roll-up |
| Labour and overhead | /labor-entries, /overhead-applications, /overhead-allocations, /overhead-closings (with preview) | Direct, step-down and reciprocal allocation |
| Process costing | /process-runs CRUD, POST /{id}/post, POST /{id}/cancel | Equivalent units, weighted average or FIFO |
| Costing reports | /costing-reports/cogm, overhead, profitability, wip, variances | Cost of goods manufactured and sold, variances |
| Scrap and direct costs | POST /production-orders/{id}/scrap, GET /production-scraps, POST /production-scraps/{id}/cancel, GET POST /direct-costs, POST /direct-costs/{id}/cancel | Normal and abnormal scrap, subcontracting and other direct costs |
| Planning | GET POST /mrp, GET POST /capacity-load | Multi-level BOM explosion, reorder suggestions, capacity vs normal hours |
| Budgets | /budgets CRUD, PUT /{id}/lines, POST /{id}/lines/from-actuals, POST /{id}/lines/from-budget, POST /budgets/spread, POST /{id}/duplicate, approve, unapprove, archive, default, GET /budget-periods | Monthly lines by account, cost center and project |
| Budget reports | GET /budgets/{id}/vs-actual, GET /budgets/{id}/flexible | Variances by section, account, cost center, project or month |
| Master budget plans | /budget-plans CRUD, POST /{id}/compute, POST /{id}/generate-budget | Operating and cash budgets, budgeted income statement |
| Management analyses | /management/cvp, variable-costing, segments, ratios | Break-even, absorption reconciliation, ROI and residual income, 19 ratios |
| Decisions and ABC | POST /management/decisions/{type}, /abc-models CRUD, POST /abc-models/{id}/compute | Make or buy, special order, keep or drop, sell or process further, limiting factor |
| Webhooks | /webhooks CRUD, GET /{id}/deliveries, POST /{id}/test, POST /{id}/deliveries/{delivery}/redeliver | Signed with HMAC SHA-256 |
| Audit logs | GET /audit-logs | Who did what, when, from where |
Idempotency, errors, limits and credits
Idempotency keys
SendIdempotency-Key(oridempotency_keyin the body) when creating an entry. A retry with the same key returns the entry that already exists, with status 200 instead of 201, so a network timeout never books a sale twice.One error shape
Every error has a machine-readablecode, a human-readablemessageand, for validation,errorskeyed by field such aslines.0.debit.Rate limits
Each key may make 120 requests per minute by default. WatchX-RateLimit-LimitandX-RateLimit-Remaining; a429carriesretry_afterin seconds.Credits in headers
Each request costs theapi.callprice and actions add their own, such asentry.create.X-Credits-ChargedandX-Credits-Balanceshow the cost and what is left; a402means the balance ran out.
Example error
HTTP/1.1 422 Unprocessable Entity
{
"message": "The entry is not balanced: debits 1,250.00 vs credits 1,200.00 (difference 50.00).",
"code": "validation_failed",
"errors": {
"lines": ["The entry is not balanced: debits 1,250.00 vs credits 1,200.00 (difference 50.00)."],
"lines.1.account_id": ["Account 9999 was not found in this fiscal year."]
}
}| Status | Meaning |
|---|---|
| 401 | Missing, invalid, expired or revoked API key |
| 402 | Not enough credits (insufficient_credits) |
| 403 | The key lacks the write ability or the permission |
| 404 | The resource does not exist in this business |
| 409 | Conflicts with the current state, such as editing a posted entry or writing into a closed year |
| 422 | Validation failed; see errors by field |
| 423 | The business or the owner's account is suspended |
| 429 | Rate limit reached; retry after retry_after seconds |
Signed webhooks for entries, accounts, parties, fiscal years and stock
Subscribe a URL to the events you need and Ledgeriano sends a JSON POST for each one. Every delivery carries X-Webhook-Event, a unique X-Webhook-Delivery id, X-Webhook-Timestamp and X-Webhook-Signature.
The signature is sha256= followed by the hex HMAC SHA-256 of timestamp + "." + raw_body, keyed with your webhook secret. Failed deliveries are retried up to 5 times with growing delays (30 seconds up to 30 minutes), and you can inspect, test and redeliver them through the API.
Events
entry.createdentry.updatedentry.postedentry.reversedentry.deletedaccount.createdaccount.updatedaccount.deletedfiscal_year.createdfiscal_year.closedparty.createdparty.updatedstock_document.postedstock_document.cancelledproduction_order.closedcosting.run_posted
A delivery
POST /webhooks/accounting
X-Webhook-Event: entry.posted
X-Webhook-Delivery: 5f0c7b1e-2a41-4c1e-9d3a-8b7f7f1c2e90
X-Webhook-Timestamp: 1790000000
X-Webhook-Signature: sha256=9c1f...e04a
{
"id": "5f0c7b1e-2a41-4c1e-9d3a-8b7f7f1c2e90",
"event": "entry.posted",
"business_id": 12,
"created_at": "2026-09-25T10:42:07+00:00",
"data": { "entry": { "number": 318, "status": "posted", "lines": [ ... ] } }
}Verify the signature
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.ACCOUNTING_WEBHOOK_SECRET;
// Keep the raw body: the signature covers the exact bytes we sent.
app.post("/webhooks/accounting", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Webhook-Timestamp") ?? "";
const signature = req.get("X-Webhook-Signature") ?? "";
const expected =
"sha256=" +
crypto.createHmac("sha256", SECRET).update(timestamp + "." + req.body.toString("utf8")).digest("hex");
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // 5 minutes
if (!valid || !fresh) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
// event.event === "entry.posted", event.data holds the entry
res.sendStatus(200);
});<?php
$secret = getenv('ACCOUNTING_WEBHOOK_SECRET');
$body = file_get_contents('php://input'); // raw body
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (! hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// $event['event'] === 'entry.posted', $event['data'] holds the entry
http_response_code(200);Accounting API questions
Is there a free way to test the accounting API?
Yes. New accounts get free credits. Create a business, generate a key and call the API; each request uses a small number of credits, shown in the X-Credits-Charged header.
Can one API key access several businesses?
No. Each key belongs to exactly one business. That keeps integrations isolated: revoking one key never affects the others.
How do I correct a posted entry through the API?
Call POST /journal-entries/{id}/reverse. Ledgeriano creates a posted reversal with debits and credits swapped and marks the original as reversed. Then post the corrected entry.
Does the API support foreign currencies?
Yes. Send currency_code and exchange_rate on the entry, or omit the rate and Ledgeriano uses the latest rate stored under /exchange-rates.
Where is the full endpoint reference?
The OpenAPI reference, rendered with Scalar, is at api.ledgeriano.com/docs/api. For a walkthrough with examples read the accounting API guide.
Build your integration today
Sign up, create a business and generate a key. Free credits cover your first tests, and every response tells you what it cost and what is left.