An accounting API is a set of HTTPS endpoints that lets software, not people, record transactions in a general ledger. Your online store sends each paid order as a journal entry, your ERP posts supplier invoices, and your BI tool reads the trial balance every night. Nobody retypes anything, and the books are current to the minute.
This guide covers the Ledgeriano accounting REST API from a developer's point of view: authentication, your first posted entry in curl and JavaScript, idempotency, error handling, webhooks, reports and a testing checklist. The full reference lives at api.ledgeriano.com/docs/api, and the developers page gives the overview.
Why automate posting with a journal entry API?
Automating posting removes the slowest and most error-prone step in small-company accounting: copying totals from one system into another. A shop with 400 orders a month and a weekly CSV export typically spends 3 to 5 hours a month importing, fixing and matching. With a bookkeeping API integration, each order becomes an entry when it is paid.
Typical integrations:
- Online store: one sales entry per paid order (or one per day, summarized).
- ERP or inventory system: purchase invoices, goods receipts and cost of sales.
- Payment gateway: fees and settlements into the bank account.
- Payroll tool: one payroll entry per month.
- Internal dashboards: read-only access to balances and reports.
| Approach | Delay | Typical errors | Audit trail |
|---|---|---|---|
| Manual entry from printouts | Days to weeks | Typos, missed invoices | Paper only |
| Monthly CSV import | About a month | Wrong mapping, duplicates on re-import | File on someone's laptop |
| Accounting API | Seconds | Caught immediately by validation | Every entry tagged with source "api" and key |
Authentication with API keys
Every business has its own API keys, and each key only sees that one business. The business owner creates, rotates and revokes keys in the dashboard (Business, then API keys). The full key is shown once, when it is created or rotated, so store it in a secret manager right away.
Send the key as a Bearer token:
Authorization: Bearer lgr_live_xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The header X-API-Key is accepted too.
Read and write abilities
Each key has one of two abilities:
| Ability | Can do | Use it for |
|---|---|---|
| read | List and read accounts, entries, parties, reports, audit logs | Dashboards, BI, reconciliation scripts |
| write | Everything read can do, plus create, post, reverse and delete | Store and ERP integrations |
Tip: Give each integration its own key. If your store is compromised you revoke one key, and the payroll integration keeps working.
Error messages come in English by default. Send Accept-Language: fa to get them in Persian.
Your first entry: create a posted journal entry with curl
A journal entry needs a date, a description and at least two lines whose debits equal the credits. Here is order 1045 from an online store in the UAE: 1,000 USD of goods plus 5% VAT, paid by card into the main bank account. The codes come from the IFRS template (1103 bank, 4101 sales of goods, 2131 VAT payable).
curl -X POST https://api.ledgeriano.com/api/v1/journal-entries \
-H "Authorization: Bearer $LEDGERIANO_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: shop-order-1045" \
-d '{
"entry_date": "2026-09-14",
"voucher_type": "SAL",
"reference": "ORDER-1045",
"description": "Online order 1045",
"status": "posted",
"tags": ["shop"],
"lines": [
{ "account_code": "1103", "debit": 1050, "description": "Card payment" },
{ "account_code": "4101", "credit": 1000, "description": "Goods sold" },
{ "account_code": "2131", "credit": 50, "description": "VAT 5%" }
]
}'
A new entry returns 201 Created with the entry in data: its id, number, status, totals, source (api) and lines. Posted entries are immutable from this moment. Leave out status (or send "draft") to create a draft that can still be edited with PUT /journal-entries/{id} and posted later with POST /journal-entries/{id}/post.
The same request in JavaScript
const res = await fetch("https://api.ledgeriano.com/api/v1/journal-entries", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.LEDGERIANO_API_KEY,
"Content-Type": "application/json",
Accept: "application/json",
"Idempotency-Key": "shop-order-" + order.id,
},
body: JSON.stringify({
entry_date: order.paidAt.slice(0, 10),
voucher_type: "SAL",
reference: "ORDER-" + order.id,
description: "Online order " + order.id,
status: "posted",
lines: [
{ account_code: "1103", debit: order.total },
{ account_code: "4101", credit: order.net },
{ account_code: "2131", credit: order.vat },
],
}),
});
const body = await res.json();
if (!res.ok) throw new Error(body.code + ": " + body.message);
console.log("Posted entry", body.data.number);
Do the rounding in your store, not in the ledger. If net + vat does not equal total to the cent, the API rejects the entry as unbalanced, which is exactly what you want.
Idempotency keys: safe retries without duplicates
An idempotency key is a unique string you send with a create request so that retrying the same request never creates a second entry. Networks fail; a timeout does not tell you whether the entry was saved. With Idempotency-Key: shop-order-1045, a retry returns the entry that already exists (status 200 instead of 201) and does not record it again.
Good keys are:
- Derived from your own record, such as the order id or invoice number, not a random value generated per attempt.
- Namespaced, for example
shop-order-1045andrefund-1045, so a refund does not collide with the sale. - Stored with your record, so you can look up the resulting entry later.
Refunds and cancelled orders
A refund is a new entry, not a change to the sale. If order 1045 is refunded in full, post the opposite lines (debit 4101 for 1,000 and 2131 for 50, credit 1103 for 1,050) with the key refund-1045 and a reference back to the order. If the sale was posted by mistake (a test order, a duplicate from an old integration), call POST /journal-entries/{id}/reverse instead: the reversal cancels it line by line and both entries stay visible to your auditor. Many teams send refunds to 4201 sales returns rather than 4101, so returns show as their own line in the income statement.
Codes vs ids: which should you send?
Send codes. Every line accepts either ids (account_id, party_id, cost_center_id, project_id) or codes (account_code, party_code, cost_center_code, project_code). In Ledgeriano the chart of accounts belongs to a fiscal year, so account 1103 in 2026 has a different id from account 1103 in 2027. The code is the stable identifier your integration can hard-code or keep in a mapping table.
| Field | Stable across fiscal years? | When to use it |
|---|---|---|
| account_code | Yes | Integrations and mapping tables |
| account_id | No, new id each year | Short-lived UI actions |
| party_code | Yes (for example C001) | Customers and suppliers from your CRM |
| voucher_type (code) | Yes (SAL, PUR, RCT, PAY and so on) | Always |
Accounts that require a party, such as 1111 trade receivables and 2111 trade payables, reject lines without one. For B2B orders, create the customer first with POST /parties and reuse its code. The chart of accounts guide explains how to design codes that integrations can rely on.
Handling errors: 422, 402, 409 and friends
Every error has the same JSON shape: a human message, a stable machine code, and (for validation) an errors object keyed by field. Handle each status on purpose instead of retrying blindly.
{
"message": "Account 4110 was not found in this fiscal year.",
"code": "validation_failed",
"errors": {
"lines.1.account_code": ["Account 4110 was not found in this fiscal year."]
}
}
| Status | Meaning | What your code should do |
|---|---|---|
| 401 | Missing, invalid, expired or revoked key | Stop and alert; do not retry |
| 402 | insufficient_credits | Pause the queue and notify the owner to top up |
| 403 | Key lacks the write ability | Fix the key configuration |
| 404 | Resource not in this business | Check ids and the key's business |
| 409 | Conflict, such as a closed fiscal year, a locked period or editing a posted entry | Move the date, reopen the year, or reverse instead of editing |
| 422 | Validation failed | Show errors per field; fix mapping or amounts |
| 423 | Business or account suspended | Stop and contact the owner |
| 429 | Rate limit reached | Wait retry_after seconds, then retry |
Reading field errors
Field keys point to the exact line: lines.1.account_code means the second line (numbering starts at 0). An unbalanced entry is reported under lines with both totals and the difference, for example "debits 1,050.00 vs credits 1,040.00 (difference 10.00)".
Warning: A 409 on
entry_dateusually means the fiscal year is closed or the period is locked. Do not "fix" it by changing the date to today in code. Posting a September sale into October distorts both months; route it to a person instead. The year-end closing guide explains lock dates and reopening.
Webhooks with signature verification
Webhooks push events to your server as JSON POST requests, so you do not have to poll. Available events include entry.created, entry.updated, entry.posted, entry.reversed, entry.deleted, account.created, account.updated, account.deleted, fiscal_year.created, fiscal_year.closed, party.created and party.updated.
Each delivery carries X-Ledgeriano-Event, X-Ledgeriano-Delivery, X-Ledgeriano-Timestamp and X-Ledgeriano-Signature. The signature is sha256= followed by the HMAC SHA-256 of the timestamp, a dot and the raw body, using the webhook's secret.
Verifying the signature in Node.js
Verify it before trusting the payload:
import crypto from "node:crypto";
export function verifyLedgeriano(rawBody, headers, secret) {
const ts = headers["x-ledgeriano-timestamp"];
const sig = headers["x-ledgeriano-signature"] || "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // older than 5 minutes
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(sig);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Use the raw request body, not a re-serialized JSON object, or the hash will not match. Failed deliveries are retried with a backoff, every delivery is logged with its response status, and you can send a test event or redeliver one from the dashboard or the API.
Reading reports via the API
The same API that writes entries also reads the results. Report endpoints live under /reports: trial-balance, balance-sheet, income-statement, cash-flow, equity-changes, general-ledger, journal, party-balances and dashboard.
curl "https://api.ledgeriano.com/api/v1/reports/trial-balance?from=2026-01-01&to=2026-09-30" \
-H "Authorization: Bearer $LEDGERIANO_API_KEY" -H "Accept: application/json"
The trial balance response lists each account with opening, period and closing debit and credit columns, totals, and an is_balanced flag. Balance sheets accept as_of and compare for the prior year. A nightly job that stores the trial balance in your data warehouse takes about 20 lines of code. For what each statement contains, see the financial statements guide.
Rate limits and credit headers
By default each key may make 120 requests per minute. Every response includes:
X-RateLimit-LimitandX-RateLimit-Remainingfor the rate limit.X-Credits-Charged: credits this call cost.X-Credits-Balance: the owner's remaining balance.
API usage is billed in credits to the business owner: each request has the API call price, and actions add their own price (recording an entry, reversing one, generating a report). Administrators set these prices, and the current ones are on the pricing page. Log the balance header and alert when it falls below a week of normal usage, so a busy sale weekend never ends in 402 errors.
One entry per order or one per day?
For high volumes, consider posting one summary entry per day per payment method instead of one entry per order. It uses fewer credits and keeps the journal readable, while your store remains the detailed record.
Testing checklist before you go live
Run through this list against a test business (create a separate business for testing so real books stay clean):
- Create a key with the write ability and confirm a
GET /accountscall works. - Post one entry per scenario: sale, refund, fee, payout.
- Retry the same request with the same Idempotency-Key and confirm you get 200 and the same entry number.
- Send an unbalanced entry and an unknown account code; confirm your code logs the 422 field errors.
- Post into a closed or locked period and confirm your code routes the 409 to a person.
- Revoke the key and confirm you handle 401 without retry loops.
- Point a webhook at a request inspector, send a test event and verify the signature.
- Compare the trial balance from the API with the one in the dashboard.
If the entries you create should also appear in a sister company, pair the API with intercompany workflows: your store posts the sale in one business and Ledgeriano records the matching purchase in the other.
How to post your first journal entry via the Ledgeriano API
- 1
Create an API key
As the business owner, open Business, then API keys, create a key with the write ability and copy it into your secret store.
- 2
Check the connection
Call GET https://api.ledgeriano.com/api/v1/accounts with the Bearer key and confirm you see the chart of accounts.
- 3
Map your accounts
Pick the account codes for each part of the transaction, for example 1103 bank, 4101 sales of goods and 2131 VAT payable.
- 4
Build a balanced payload
Include entry_date, description, voucher_type and lines whose debits equal the credits; add status posted if it should post immediately.
- 5
Send it with an Idempotency-Key
POST the payload to /journal-entries with a key derived from your own record, such as shop-order-1045.
- 6
Handle the response
Store the returned entry id and number on 201 or 200, and log field errors on 422 or pause on 402.
- 7
Verify in the ledger
Open the journal or trial balance in the dashboard, or call /reports/trial-balance, and confirm the entry appears as expected.
Frequently asked questions
What is an accounting API?
+
An accounting API is a web interface that lets other software create journal entries, manage accounts and parties, and read reports in an accounting system. It replaces manual data entry and CSV imports with automatic, validated posting.
How do I post a journal entry through an API?
+
Send a POST request to the journal entries endpoint with a date, description and at least two lines whose debits equal the credits, authenticated with an API key. In Ledgeriano, add "status": "posted" to post it immediately, or leave it as a draft to review first.
How do I connect my online store to accounting software?
+
Listen for paid orders in your store (a webhook or a scheduled job), map each order to accounts such as bank, sales and VAT payable, and post it with the order id as the Idempotency-Key. Refunds become separate entries, never edits.
What is an idempotency key in an accounting API?
+
It is a unique value, such as an order id, sent with a create request. If the request is retried, the API returns the entry that already exists instead of creating a duplicate, so network errors never double your revenue.
Can I edit a posted entry through the API?
+
No. Posted entries are immutable and the API answers 409 if you try. Reverse the entry with POST /journal-entries/{id}/reverse and post a corrected one, which keeps a complete audit trail.
Is the Ledgeriano API free to use?
+
API calls use credits from the business owner's balance, like actions in the dashboard. New accounts receive free credits, and every response reports the credits charged and the remaining balance in headers.
Does the API support webhooks?
+
Yes. You can subscribe to events such as entry.posted, entry.reversed and account.created. Deliveries are signed with HMAC SHA-256, logged, retried on failure and can be redelivered manually.
Reviewed by our accounting specialists. Published:



