Skip to content

Search documentation

Search documentation pages, sections, and topics.

On this page

Transactions

When money moves you have to record two things: where it came from and where it went. Miss one and your books will not add up, and you will not be able to tell why.

A transaction records both at once, and it is the only way to change a balance in Ledfra. There is no endpoint that sets a balance directly. 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 does not balance, Ledfra rejects it, so money can never appear from nowhere or quietly vanish.

Think of it as conservation of money. It is never created or destroyed, only moved from one account to another, and each entry records one end of that move. An accountant calls the resulting record a journal entry.

Debits and credits are not plus and minus

A debit is not “good” and a credit is not “bad” - they are 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 simply the side it increases on: debit for assets and expenses, credit for liabilities, income, and equity. It follows the category the account is filed under (see Accounts).

Anatomy of a transaction

A transaction is a short description, optional metadata, and a list of entries. Every entry has three fields:

  • account - the account slug it posts to, like assets:cash.
  • operation - DEBIT 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.

Put your own identifiers in metadata
The description should classify the transaction so a reviewer can scan it (“Booking paid”, “Payout to host”). Your ids (payment intent, order, booking) belong in metadata, where they stay machine-readable. That pairing is what makes an entry traceable back to the event in your product that caused it.

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",
    "metadata": { "paymentIntentId": "pi_3Nx9c21" },
    "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 do not 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 (request body only 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 - never edited, never 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. It is also why your trial balance shows both movements rather than a net of zero.

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 across:

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 is pending and crediting the available one raises what the host can take out. What you owe the host in total does not change - it just became theirs to withdraw. Modeling each state money passes through is a design rule, covered in How to design your ledger.

Retry-safe writes

Network failures happen, and a timed-out POST may or may not have been written. Send an Idempotency-Key header with one stable value per real-world event (a booking id, a payout id), and a retry returns the original transaction instead of posting a second one. Keys are scoped to the ledger and may be up to 255 characters.

A key identifies a transaction, not a request body
Reusing a key with a different body still returns the original transaction. Ledfra does not compare bodies and does not warn you, so treat every key as belonging to exactly one transaction and generate a fresh key for a genuinely new one. Deriving the key from your own source event, rather than from a random value per attempt, is what makes this safe in practice.

Posting and reading

Post with POST /ledgers/{ledgerId}/transactions and read history with GET /ledgers/{ledgerId}/transactions, which returns most-recent-first with cursor pagination and optional since / till bounds. A transaction carries 2 to 1000 entries and is written atomically - it either lands whole or not at all.

Full request and response shapes, plus idempotent writes, are in the API reference.

Where to go next

  1. How to build a marketplace ledger - these patterns applied end to end in a real business.
  2. How to design your ledger - deciding which accounts these entries should post against.
  3. Daily activity - see the transactions you posted, with the day's totals around them.
  4. What is Ledfra? - why balancing is enforced at the API boundary rather than checked afterwards.
  5. Guide: Model normal debit and credit accounts - what a debit actually is, and why an account has one side that makes it go up.
  6. API reference - every endpoint and error.