Transactions

When money moves, you have to record two things: where it came from and where it went. Miss one and your books won't add up — and you won't be able to tell why. A transaction is how you record both at once. It's the only way to change a balance in Ledfra, and every transaction follows one rule: it must balance.

What double-entry means

Ledfra is a double-entry ledger. Every transaction touches at least two accounts, and the amounts on the two sides — debits and credits — must be equal. Because each movement is written on both sides, the books are self-checking: if a transaction doesn't balance, Ledfra rejects it, so money can never appear from nowhere or quietly vanish.

Think of it as conservation of money. It's never created or destroyed, only moved from one account to another — and each entry records one end of that move.

Debits and credits aren't plus and minus

A debit isn't "good" and a credit isn't "bad" — they're just the two sides of a move. Which one increases an account depends on the account's type:

Account typeA debit…A credit…
Asset (cash, receivables)increasesdecreases
Expense (fees)increasesdecreases
Liability (payables, user balances)decreasesincreases
Income (revenue)decreasesincreases
Equitydecreasesincreases

An account's normal balance is just the side it increases on — debit for assets and expenses, credit for liabilities, income, and equity. That's the normalBalance you set on each account (see Accounts).

Anatomy of a transaction

A transaction is a short description plus a list of entries. Every entry has three fields:

  • account — the account slug it posts to, like assets:cash.
  • operationDEBIT or CREDIT.
  • amount — a money string like USD:50.00 (see Money).

Two rules hold every time: a transaction needs at least two entries, and within each currency, total debits must equal total credits. When one transaction bundles more than one independent pair, tag each pair with the same doubleEntryId so Ledfra knows which entries offset which.

Examples

Move money between two accounts

The simplest transaction is one debit and one credit. A user tops up their wallet with $50: cash comes in (a debit to your asset), and you now owe the user that $50 (a credit to their liability balance).

bash
curl 'https://ledfra.com/api/ledgers/{ledgerId}/transactions' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Idempotency-Key: wallet-topup-9c21' \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Wallet top-up",
    "entries": [
      { "account": "assets:cash",          "operation": "DEBIT",  "amount": "USD:50.00" },
      { "account": "liabilities:user/123", "operation": "CREDIT", "amount": "USD:50.00" }
    ]
  }'

Split one side into several

Debits and credits don't have to be one-to-one — only the totals must match. A $50 sale where $45 is revenue and $5 is sales tax is one debit balanced by two credits (we show just the request body from here):

json
{
  "description": "Sale with tax",
  "entries": [
    { "account": "assets:cash",     "operation": "DEBIT",  "amount": "USD:50.00" },
    { "account": "income:sales",    "operation": "CREDIT", "amount": "USD:45.00" },
    { "account": "liabilities:tax", "operation": "CREDIT", "amount": "USD:5.00" }
  ]
}

Undo something: post the reverse

Transactions are a permanent record — they're never edited or deleted. To undo one, post a new transaction that mirrors it. Refunding the wallet top-up simply swaps the debit and credit:

json
{
  "description": "Refund wallet top-up",
  "entries": [
    { "account": "liabilities:user/123", "operation": "DEBIT",  "amount": "USD:50.00" },
    { "account": "assets:cash",          "operation": "CREDIT", "amount": "USD:50.00" }
  ]
}
Reversing rather than deleting keeps the full history intact — you can always see that money came in and later went back out, which is exactly what an audit needs.

Hold funds, then release them

Marketplaces rarely pay out the moment money arrives — you hold a host's earnings until the stay is over, then make them withdrawable. Model it with two liability accounts per host: a locked one for pending earnings and an available one they can withdraw from. The booking credits the locked account; when the stay completes, you move the money from locked to available:

json
{
  "description": "Release host earnings at checkout",
  "entries": [
    { "account": "liabilities:hosts-locked/42", "operation": "DEBIT",  "amount": "USD:200.00" },
    { "account": "liabilities:hosts/42",        "operation": "CREDIT", "amount": "USD:200.00" }
  ]
}

Both accounts are liabilities, so debiting the locked one lowers what's pending and crediting the available one raises what the host can take out. What you owe the host in total doesn't change — it just became theirs to withdraw.

Posting and reading

Post a transaction with POST /ledgers/{ledgerId}/transactions and read history with GET /ledgers/{ledgerId}/transactions. Send an Idempotency-Key header on writes so a network retry can't post the same transaction twice.

Make writes retry-safe
Use one stable Idempotency-Key per real-world event. A retry with the same key returns the original transaction instead of creating a duplicate — see Idempotent writes in the API reference.

Where to go next

For a complete, end-to-end story — designing accounts, posting a booking, paying out, and reading reports — follow the Marketplace example. For the exact request and response shapes, see the API reference.