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 slug 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 slug.

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 slug
How manyA handful you designOne per object, unbounded
Exists fromThe moment you design itIts first transaction
Example slugassets: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 slugs: 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.

Slugs 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 slug 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

Two conventions are worth adopting from the start, because accounts are referenced by posted history and are not safely renamable afterwards.

  • 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”.

Listing and fetching accounts

  • GET /ledgers/{ledgerId}/accounts lists a ledger's accounts and is how you discover valid slugs. It supports a query substring filter over names and slugs, 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.

Slugs 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 slug)
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 slug
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 slug 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 slugs that map to something you have already designed. A slug matching neither a control account nor a template is rejected, so you cannot invent arbitrary accounts just by posting to them.
  • An account holds a single currency.

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.