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 type | A debit… | A credit… |
|---|---|---|
| Asset (cash, receivables) | increases | decreases |
| Expense (fees) | increases | decreases |
| Liability (payables, user balances) | decreases | increases |
| Income (revenue) | decreases | increases |
| Equity | decreases | increases |
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, likeassets:cash.operation-DEBITorCREDIT.amount- a money string likeUSD: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.
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).
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):
{
"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:
{
"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" }
]
}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:
{
"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.
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
- How to build a marketplace ledger - these patterns applied end to end in a real business.
- How to design your ledger - deciding which accounts these entries should post against.
- Daily activity - see the transactions you posted, with the day's totals around them.
- What is Ledfra? - why balancing is enforced at the API boundary rather than checked afterwards.
- Guide: Model normal debit and credit accounts - what a debit actually is, and why an account has one side that makes it go up.
- API reference - every endpoint and error.