Transactions

A transaction is one business fact — a date, a narration, and a set of entries. It carries no amount of its own. The money lives entirely in its entries, whose debits equal their credits.

Amounts are integers in minor units. 150000 means ₹1,500.00. Decimals are refused, not rounded.

The model in one line

A transaction is a dated fact. Its entries are how it lands on accounts. The entries' debits equal their credits.


POST /api/v1/transactions

Create a transaction and its entries in one atomic call. Either the whole thing is written or nothing is — a rejected call leaves no partial rows behind, so retrying after a validation error is safe.

Request

{
  "date": "2026-08-05",
  "narration": "Rent paid — Aug 2026",
  "type": "vendor_paid",
  "status": "posted",
  "source": "sap",
  "source_ref": "4900001234",
  "entries": [
    { "account": "a1b2c3d4e5f6", "debit": 3000000, "credit": 0, "dimensions": { "cost_centre": "Mumbai" } },
    { "account": "a1b2c3d4e5f6", "debit": 2000000, "credit": 0, "dimensions": { "cost_centre": "Delhi" } },
    { "account": "f6e5d4c3b2a1", "debit": 0, "credit": 5000000 }
  ]
}
FieldRequiredNotes
dateyesYYYY-MM-DD. The accounting date, not when it was keyed in
entriesyes for postedArray; see below
timenoHH:MM or HH:MM:SS, wall-clock as the source printed it
narrationnoWhat happened, in words
typenoBusiness event, free text — vendor_paid, sales_recorded
statusnoposted (default) or draft
currencynoDefaults to INR
sourcenomanual (default), or the upstream system's name
source_refnoThe upstream document number
detailsnoThe upstream event as it arrived, any shape

Each entry takes account, one of debit or credit, and optionally memo and dimensions.

What is enforced

  1. Debits equal credits for anything reaching posted
  2. At least two entries on a posted transaction
  3. No entry against a group account — post to its children
  4. Every account exists in this organization and is postable

Drafts are exempt from 1 and 2: a draft is work in progress, and refusing to save a half-built one would leave nowhere to put it.

Idempotency

When source and source_ref are both given, they are the transaction's identity. Re-posting one already sent returns the existing transaction with created: false rather than creating a second copy:

{ "data": { "id": "...", "created": false, "...": "..." } }

This is what makes an interrupted sync safe to re-run — for a shadow ledger the normal case, not an edge one.

A re-post does not update the existing transaction. A posted transaction is immutable; correcting one means voiding it and posting a replacement.

Response

201 with created: true, or 200 with created: false. The body carries the transaction, its entries, and total_debit / total_credit.


GET /api/v1/transactions

ParamNotes
date_from, date_toInclusive, YYYY-MM-DD
statusdraft, posted or void
typeExact match on the business event
source, source_refUpstream identity; together they resolve one row
accountOnly transactions with an entry against this account
dimension.<key>e.g. dimension.cost_centre=Mumbai
qSubstring match on narration
include_entriesfalse omits entries; they are included by default
sort_orderASC or DESC on date, default DESC
page, limitDefault 100 per page, max 500

Entries come back with each transaction by default, along with total_debit and total_credit, so a list is usable without a follow-up call per row.

Results are ordered date, time NULLS LAST, created_at, id — deterministic whether or not a source supplied a time.

# Everything hitting one account in Q1, with its entries
curl "https://ledger.finopsbricks.com/api/v1/transactions?account=a1b2c3d4e5f6&date_from=2026-01-01&date_to=2026-03-31" \
  -H "api-key: $KEY" -H "api-secret: $SECRET"

GET /api/v1/transactions/:id

One transaction with its entries and totals.


PATCH /api/v1/transactions/:id

Two different operations, one at a time.

Changing status

{ "status": "posted" }

The lifecycle:

draft ──post──> posted ──void──> void
  └───void───────────────────────┘

posted is the point of no return. The balance check bites on the way there. After it, a mistake is corrected by voiding and posting a replacement — which is what keeps a shadow ledger reconcilable against the system it mirrors.

Editing content

Drafts only. Send any of date, time, narration, type, currency, details, entries.

entries is replaced wholesale, not merged. Sending three entries leaves the transaction with exactly those three. Patching individual lines would let a caller leave a transaction half-edited with only the eventual balance check noticing.

Editing a posted transaction returns TRANSACTION_IMMUTABLE (409).


DELETE /api/v1/transactions/:id

Drafts only. A posted transaction is voided, never deleted — removing the record that a document was mirrored defeats the purpose of mirroring it.


Dimensions

Dimensions sit on the entry, not the transaction. The rent example above is why: one payment split across two branches has expense lines under different cost centres and a bank line under none. That shape cannot be expressed a level up.

Keys are yours to choose. Filter with dimension.<key>=<value> on either this endpoint or entries.