Skip to content
Developers

Accounting API: Post Journal Entries From Your Store or ERP Automatically

An accounting API lets your online store, ERP or billing system create balanced journal entries, read balances and pull financial reports over HTTPS, so sales reach the ledger in seconds instead of being retyped at month end.

By the Ledgeriano editorial team10 min readUpdated:
Accounting API request posting a balanced journal entry from an online store to Ledgeriano, with the JSON response

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.
ApproachDelayTypical errorsAudit trail
Manual entry from printoutsDays to weeksTypos, missed invoicesPaper only
Monthly CSV importAbout a monthWrong mapping, duplicates on re-importFile on someone's laptop
Accounting APISecondsCaught immediately by validationEvery 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:

AbilityCan doUse it for
readList and read accounts, entries, parties, reports, audit logsDashboards, BI, reconciliation scripts
writeEverything read can do, plus create, post, reverse and deleteStore 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:

  1. Derived from your own record, such as the order id or invoice number, not a random value generated per attempt.
  2. Namespaced, for example shop-order-1045 and refund-1045, so a refund does not collide with the sale.
  3. 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.

FieldStable across fiscal years?When to use it
account_codeYesIntegrations and mapping tables
account_idNo, new id each yearShort-lived UI actions
party_codeYes (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."]
  }
}
StatusMeaningWhat your code should do
401Missing, invalid, expired or revoked keyStop and alert; do not retry
402insufficient_creditsPause the queue and notify the owner to top up
403Key lacks the write abilityFix the key configuration
404Resource not in this businessCheck ids and the key's business
409Conflict, such as a closed fiscal year, a locked period or editing a posted entryMove the date, reopen the year, or reverse instead of editing
422Validation failedShow errors per field; fix mapping or amounts
423Business or account suspendedStop and contact the owner
429Rate limit reachedWait 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_date usually 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-Limit and X-RateLimit-Remaining for 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):

  1. Create a key with the write ability and confirm a GET /accounts call works.
  2. Post one entry per scenario: sale, refund, fee, payout.
  3. Retry the same request with the same Idempotency-Key and confirm you get 200 and the same entry number.
  4. Send an unbalanced entry and an unknown account code; confirm your code logs the 422 field errors.
  5. Post into a closed or locked period and confirm your code routes the 409 to a person.
  6. Revoke the key and confirm you handle 401 without retry loops.
  7. Point a webhook at a request inspector, send a test event and verify the signature.
  8. 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. 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. 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. 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. 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. 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. 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. 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:

Intercompany transactions automation: a posted sales voucher in a wholesaler creating a matching purchase voucher in a retailer
Automation12 min read

Intercompany Transactions Automation: Record Both Sides of Every Deal Once

Intercompany transactions automation means that when one company posts a sale, invoice or payment to a sister company, the matching entry is created in the other company's books automatically, with the same amount, date and reference, so both sides always agree.

Read guide →
Double-entry bookkeeping online: a balanced journal entry with debit and credit lines next to a trial balance
Operations11 min read

Double-Entry Bookkeeping Online: Journal Entry Examples and the Trial Balance

Double-entry bookkeeping records every transaction in at least two accounts, with total debits equal to total credits, so the books always balance. Online, you enter each transaction as a journal entry (voucher), post it when it is reviewed, and the trial balance proves the ledger still balances.

Read guide →

Put this guide into practice

Create your first business and record a balanced entry in a few minutes. New accounts get free credits.