Skip to content

Search documentation

Search documentation pages, sections, and topics.

On this page

Accounts & templates

A ledger is a chart of accounts, and every transaction entry posts to one of them. Some you can plan up front: cash, revenue, processing fees. Others you cannot: a running balance for each of your users, sellers, or tenants, where there may be millions and new ones appear every day.

Ledfra handles both, and you address every account the same way, by a readable key like income:rent or liabilities:user/123 - never a bare number like 2100 whose meaning lives in a separate lookup table. (Traditional accounting systems call that number a GL code.)

The two kinds of account

Every account is one of two kinds. The difference is simply who creates it, and when.

Control accounts

A control account (type: CONTROL) is a standalone bookkeeping account you design up front in the Ledfra app: cash, revenue, processing fees. There is a small, known set of them, you name them yourself, and the API only ever reads them. Use control accounts for the figures your whole business shares: the lines you would put on a balance sheet by name.

Template accounts

A template account (type: TEMPLATE) is a per-object subsidiary account: one wallet per user, one payable per seller, one receivable per tenant. You do not design each one. You design the template once (its currency, normal balance, and category), and Ledfra creates the individual account the first time a transaction references its key.

That is two objects with two similar names, so it is worth pinning them down. The pattern you design once is an account template. Each account it creates is a template account, and the template account is the one that holds a balance and gets posted to. Some other ledgers use the phrase “template account” for the pattern itself, so when you are reading another API, check which of the two it means.

Control accountTemplate account
Who creates itYou, in the Ledfra appLedfra, on the first transaction that uses its key
How manyA handful you designOne per object, unbounded
Exists fromThe moment you design itIts first transaction
Example keyassets:cash, income:rentliabilities:user/123, receivables:tenant/42
Use it forAccounts your whole business sharesOne balance per external object
Control accounts versus template accountsControl accounts are a small fixed set you design (assets:stripe, assets:paypal, expenses:payment-processing-fees). Template accounts (liabilities:payables, assets:receivables) expand to one account per object, e.g. assets:receivables/42.Control accountsA small, fixed set you design.assets:stripeassets:paypal$expenses:payment-processing-feesTemplate accountsOne per object, created on first use.liabilities:payablesassets:receivables↳ assets:receivables/42↳ assets:receivables/99↳ assets:receivables/127
Control accounts are a fixed set you design, template accounts expand to one per object.

Why keys: accounts created on the fly

The obvious way to track a balance per user is to create an account row for every user before you can record their first transaction. That means a second API call, error handling for when it already exists, and a race when two events for a new user arrive at once. It is busywork, and it is easy to get wrong.

Keys remove the step entirely. You reference an account by a stable, readable name, and posting a transaction is a transparent upsert: if the template account exists we use it, and if it does not we create it from its template and then post, all in one call. You never call a “create account” endpoint, because there isn't one.

bash
# 'liabilities:user/123' doesn't exist yet. Ledfra creates it from the
# 'user' template, then posts the entry — one call, no pre-provisioning.
curl 'https://ledfra.com/api/ledgers/{ledgerId}/transactions' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Deposit to user wallet",
    "entries": [
      { "account": "assets:cash",          "operation": "DEBIT",  "amount": "USD:100.00" },
      { "account": "liabilities:user/123", "operation": "CREDIT", "amount": "USD:100.00" }
    ]
  }'
It's a design choice for developer experience
No pre-provisioning, no create-then-post two-step, and no races to manage - post to liabilities:user/123 and the account is simply there. The key doubles as an audit trail: it says exactly what the account is and which object it belongs to, in every transaction that touches it.

Each account also carries a normalBalance (CREDIT or DEBIT), the side on which its balance increases, and the currency it is denominated in. You do not pick the normal balance per account: it follows the category you file the account under, which How to design your ledger covers. For how amounts are represented, see Money.

Naming and keying accounts

An account’s key is what your code posts against, so it is the part of a chart of accounts that costs the most to change later. Three conventions are worth adopting from the start.

  • Key per-object accounts by a stable id, never a name or email. liabilities:payables-hosts/9001 survives the host changing their name. liabilities:payables-hosts/jane@example.com does not.
  • Name the leaf, not the path. The hierarchy already carries the context and the app renders the breadcrumb, so an account under Assets › Current Assets is called “Stripe”, not “Current Asset Stripe Balance”.
  • Put the currency in the key once a ledger holds more than one. assets:stripe:usd and assets:stripe:eur are two accounts, because an account holds one currency for its lifetime. The suffix is a convention rather than something Ledfra reads: an account’s currency is a field you set when you create it. What it buys you is a chart of accounts that is readable at a glance, where two Stripe balances do not look like someone made a duplicate.

Renaming an account key

Renaming used to be a breaking change with no warning. The old key stopped resolving, and any integration still posting to it started failing. A rename now keeps the previous key working for a grace period you choose at the time: 7, 30, or 90 days, defaulting to 30. Postings to the old name keep landing on the same account until it lapses.

The grace period covers writes, not reads. GET /ledgers/{ledgerId}/accounts returns the new key from the moment of the rename, so code that stores a key and compares it as a string still sees a change on the next read. Use the window to migrate your callers, not as a permanent second name.

Renaming an account template moves its accounts with it. A per-object account is addressed as template-key/object-id, so liabilities:payables-hosts/9001 follows the template to its new name automatically. Nothing is rewritten and nothing is left behind on the old prefix.

One limit worth knowing before you plan a swap: while a previous key is still inside its grace period it continues to occupy that name, so you cannot immediately hand it to a different account. Wait for the window to lapse, or rename to a name nothing has used.

Listing and fetching accounts

The path parameter below is spelled slug. That is the same thing this page and the Ledfra app call an account's key - one identifier, and the older name kept in the URL so existing integrations keep working.

  • GET /ledgers/{ledgerId}/accounts lists a ledger's accounts and is how you discover valid keys. It supports a query substring filter over names and keys, with cursor pagination.
  • GET /ledgers/{ledgerId}/accounts/{slug} fetches a single account. A template account returns 404 until its first transaction brings it into existence.
  • GET /ledgers/{ledgerId}/accounts/{slug}/balance returns the live balance, computed from the ledger rather than cached.

Keys may contain /, so URL-encode one when it appears in a path: receivables:tenant/123 becomes receivables:tenant%2F123.

bash
# List accounts (optionally filter by name or key)
curl 'https://ledfra.com/api/ledgers/{ledgerId}/accounts?query=tenant' \
  -H 'Authorization: Bearer YOUR_API_KEY'

# Fetch one account — note the URL-encoded '/' in the key
curl 'https://ledfra.com/api/ledgers/{ledgerId}/accounts/receivables:tenant%2F123' \
  -H 'Authorization: Bearer YOUR_API_KEY'

# And its live balance
curl 'https://ledfra.com/api/ledgers/{ledgerId}/accounts/assets:cash/balance' \
  -H 'Authorization: Bearer YOUR_API_KEY'

Limitations

Know these edges
  • A template account does not exist until its first transaction - fetching it by key returns 404 before then.
  • You cannot create accounts of any kind through the API. Control accounts and templates are designed in the app. Individual template accounts come into being only through the upsert above.
  • The upsert only works for keys that map to something you have already designed. A key matching neither a control account nor a template is rejected, so you cannot invent arbitrary accounts just by posting to them.
  • An account holds one currency for its lifetime. A ledger can hold accounts in several, but Ledfra never converts between them.

Where to go next

  1. How to design your ledger - deciding which accounts to create, and how to structure them.
  2. Transactions - how entries post against these accounts.
  3. Quickstart - build a small chart of accounts and use it.
  4. What is Ledfra? - where accounts sit in the model.
  5. API reference - the account endpoints in full.