Skip to main content

What the Ledger Records

Recurso includes a built-in double-entry ledger that automatically records journal entries for every financial event — payments, invoices, refunds, and more. Every transaction creates balanced debits and credits, giving you an auditable, real-time view of your financial position.

Why Double-Entry?

  • Accuracy — Every transaction balances to zero. If debits do not equal credits, the entry is rejected.
  • Auditability — Full history of every financial movement, tied back to source objects (invoices, payments, refunds).
  • Reporting — Generate balance sheets, income statements, and cash flow reports directly from the ledger.
  • Compliance — Meets requirements for ASC 606 revenue recognition and standard accounting principles.

Account Types

Recurso organizes ledger accounts into five standard accounting categories:

Built-in Account Codes

Recurso provisions the following accounts automatically for each tenant:
Account codes follow standard chart-of-accounts conventions. Assets start with 1xxx, liabilities with 2xxx, revenue with 4xxx, and expenses with 5xxx.

How Transactions Are Created

Recurso automatically creates journal entries when financial events occur. You never need to create entries manually — they are produced as a side effect of normal billing operations.

Invoice Finalized

When an invoice is finalized, Recurso records the amount owed:

Payment Received

When the customer pays the invoice:
Across both events the postings balance — every debit is matched by an equal credit, and Accounts Receivable nets back to zero once the invoice is paid:

Revenue Recognized

As the subscription period elapses, the scheduler earns deferred revenue month by month:
One-off (non-subscription) invoices skip deferral entirely — their Code-1 entry credits 4000 Revenue directly, because the charge is earned the moment it is billed.

Refund Issued

When a refund is processed:

Tax Collected

The invoice entry (Code 1) posts the GROSS total to Accounts Receivable, so the tax portion starts inside Deferred Revenue. A separate reclass entry (Code 6) moves it to its own liability — note it debits Deferred, not AR, which already carries the gross amount:
(For a one-off invoice the reclass debits 4000 Revenue instead.)

Discounts

Discounts do not post their own entry. A coupon reduces the invoice total before Code 1 posts, so the ledger simply records the smaller gross amount — there is nothing separate to reclass.

Querying the Ledger

List Ledger Accounts

Retrieve all ledger accounts and their current balances.

List Journal Entries

Query the postings that touch one account. account_id is required; filter by posting code and page with limit/offset. Each row is one balanced transfer — a single debit account, a single credit account, one amount:

Entry Filter Parameters

reference_id is the business object that caused the posting (invoice, payment, credit note, wallet transaction, …); there is no date-range filter — page through by created_at order instead.

LedgerAccount Object

LedgerTransaction Object

Every transaction is exactly one debit and one credit — there is no multi-line entries[] array. A business event that touches several accounts (an invoice with tax, say) posts several rows sharing the same reference_id.

TigerBeetle Backend

For high-throughput environments, Recurso supports an optional TigerBeetle backend. TigerBeetle is a purpose-built financial transactions database that provides:
  • Sub-millisecond latency for ledger operations
  • Strict serializability guaranteeing consistency
  • Built-in two-phase transfers for complex settlement flows
  • Throughput exceeding 1 million transactions per second
TigerBeetle is optional. The default PostgreSQL-backed ledger handles most workloads. Consider TigerBeetle if you process more than 10,000 transactions per minute or need sub-millisecond ledger writes.
To enable TigerBeetle, set its address (unset = Postgres only):
.env
Every posting is dual-written: PostgreSQL is written first and is always authoritative; when TigerBeetle is connected, the same transfer is mirrored to it by transaction ID. If TigerBeetle is unavailable, Postgres still holds the complete, correct ledger. The API surface remains identical — switching backends requires no changes to your integration code.

Reconciliation

The reconciliation report answers “does the ledger agree with the billing records?” for your tenant. Run it on demand with GET /v1/finance/reconciliation; a scheduler also runs it daily and logs Ledger reconciliation found discrepancies when drift appears. The report is computed fresh on every run and never persisted — it only reads, fixing drift is a human decision.
Two passes run:
  1. Billing records vs. PostgreSQL ledger — every non-draft invoice must have invoice postings summing to its total, every paid invoice must have payment postings summing to its amount paid, and every such posting must reference an existing invoice.
  2. PostgreSQL ledger vs. TigerBeetle (when connected) — the tenant’s Postgres ledger transactions (authoritative) are diffed against the enumerated TigerBeetle transfers by transaction ID.

Discrepancy types

tb_compared and tb_skip_reason

The TigerBeetle pass is best-effort and always honest: when it cannot run, the report sets tb_compared: false with the reason in tb_skip_reason instead of failing. Skip reasons you may see:
  • TigerBeetle is not configured or the client is not connected (normal for Postgres-only deployments — Postgres is the authoritative ledger)
  • The tenant has more than 100,000 ledger transactions or transfers, above the in-memory comparison guard
  • Enumerating transfers or loading transaction summaries failed
When the pass runs, tb_compared is true and tb_accounts_checked / tb_transfers_checked report its scope.
The report lists at most 100 discrepancies (truncated: true when more exist); total_discrepancies always carries the full count.

Watching ledger activity

There are no ledger.* webhook event types. Every posting is written synchronously with the billing event that caused it, so subscribe to the business events instead (invoice.created, payment.succeeded, …) and read the postings with the entries endpoint when you need them.

Best Practices

Use Reference IDs

Always trace ledger entries back to their source invoice, payment, or refund using reference_type and reference_id

Reconcile Regularly

Compare ledger balances against your payment gateway settlements weekly to catch discrepancies early

Monitor Imbalances

Set alerts on the trial balance. Any non-zero difference indicates a bug or data corruption

Archive Old Entries

Query with date ranges to keep responses fast. Export older entries to your data warehouse for long-term storage
Ledger entries are immutable. To correct an error, create a reversing entry (debit and credit swapped) rather than deleting or modifying the original transaction.