Skip to content
Developers

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, webhooks and the audit trail. JSON in, JSON out, with an OpenAPI reference you can try in the browser.

Accounting API for developers: code posting a journal entry to the Ledgeriano REST API
JSON over HTTPS, OpenAPI documented
REST
owner-managed API keys
Bearer
default rate limit per key
120/min
webhook event types
12

Base URL

https://api.ledgeriano.com/api/v1

Every business has its own keys; the key decides which business you are talking to.

Quickstart

From zero to a posted journal entry in three steps

  1. 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. 2

    Make a read call

    Send GET /accounts with Authorization: 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. 3

    Post an entry

    Send POST /journal-entries with balanced lines and an Idempotency-Key. Use account codes or ids, add "status": "posted" to post immediately, or leave it as a draft for review.

Code samples

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 $LEDGERIANO_API_KEY" \
  -H "Accept: application/json" \
  -H "Accept-Language: fa"   # Persian error messages and labels
curl -X POST https://api.ledgeriano.com/api/v1/journal-entries \
  -H "Authorization: Bearer $LEDGERIANO_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 }
    ]
  }'
Authentication

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.

request headers
Authorization: Bearer lgr_live_xxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json
Accept-Language: en        # or fa
Idempotency-Key: order-10482   # on create
readwriterotaterevokeexpires_at
Resources

What 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).

What the API covers
ResourceEndpointsNotes
Fiscal yearsGET POST /fiscal-years, GET PUT DELETE /fiscal-years/{id}, POST /{id}/close, POST /{id}/reopenClosing writes the closing and opening entries
Accounts/accounts CRUD, GET /accounts/{id}/balanceChart of accounts of the current fiscal year
Parties/parties CRUDCustomers and suppliers
Cost centers/cost-centers CRUDDepartments, branches
Projects/projects CRUDJobs and contracts
Voucher types/voucher-types CRUDCodes such as SAL, PUR, PAY
Journal entries/journal-entries CRUD, POST /bulk-post, POST /{id}/post, POST /{id}/reverse, POST /{id}/duplicate, /{id}/attachmentsDrafts can be edited; posted entries are corrected by reversal
Exchange ratesGET 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, dashboardSame numbers as the dashboard
Webhooks/webhooks CRUD, GET /{id}/deliveries, POST /{id}/test, POST /{id}/deliveries/{delivery}/redeliverSigned with HMAC SHA-256
Audit logsGET /audit-logsWho did what, when, from where
Reliability

Idempotency, errors, limits and credits

  • Idempotency keys

    Send Idempotency-Key (or idempotency_key in 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-readable code, a human message in English or Persian (Accept-Language: fa) and, for validation, errors keyed by field such as lines.0.debit.
  • Rate limits

    Each key may make 120 requests per minute by default. Watch X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries retry_after in seconds.
  • Credits in headers

    Each request costs the api.call price and actions add their own, such as entry.create. X-Credits-Charged and X-Credits-Balance show the cost and what is left; a 402 means the balance ran out.

Example error

application/json
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."]
  }
}
Example error
StatusMeaning
401Missing, invalid, expired or revoked API key
402Not enough credits (insufficient_credits)
403The key lacks the write ability or the permission
404The resource does not exist in this business
409Conflicts with the current state, such as editing a posted entry or writing into a closed year
422Validation failed; see errors by field
423The business or the owner's account is suspended
429Rate limit reached; retry after retry_after seconds
Webhooks

Signed webhooks for entries, accounts, parties and fiscal years

Subscribe a URL to the events you need and Ledgeriano sends a JSON POST for each one. Every delivery carries X-Ledgeriano-Event, a unique X-Ledgeriano-Delivery id, X-Ledgeriano-Timestamp and X-Ledgeriano-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.created
  • entry.updated
  • entry.posted
  • entry.reversed
  • entry.deleted
  • account.created
  • account.updated
  • account.deleted
  • fiscal_year.created
  • fiscal_year.closed
  • party.created
  • party.updated

A delivery

POST /webhooks/ledgeriano
X-Ledgeriano-Event: entry.posted
X-Ledgeriano-Delivery: 5f0c7b1e-2a41-4c1e-9d3a-8b7f7f1c2e90
X-Ledgeriano-Timestamp: 1790000000
X-Ledgeriano-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.LEDGERIANO_WEBHOOK_SECRET;

// Keep the raw body: the signature covers the exact bytes we sent.
app.post("/webhooks/ledgeriano", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Ledgeriano-Timestamp") ?? "";
  const signature = req.get("X-Ledgeriano-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);
});
FAQ

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; after that you pay per call as listed on the pricing page.