Accounts

An account is a name and a type. Everything else about it — which side it normally sits on, which statement it lands on — derives from type and is never stored, so it can never drift.

TypeNormal balanceStatement
assetdebitbalance_sheet
liabilitycreditbalance_sheet
equitycreditbalance_sheet
incomecreditincome_statement
expensedebitincome_statement

Responses include normal_balance and statement so you never have to reconstruct this table.

Groups and hierarchy

is_group: true marks a heading that exists only to roll up its children. Nothing can post to one. A group's balance is the sum of its descendants, so a report must not show a group and its children as separate figures for the same money.

Being a group is independent of having children. Travel Expense can carry its own miscellaneous entries while Airfare and Hotels hang beneath it; a newly created heading has no children yet and must still refuse entries.

A parent must be a group of the same type. A group's total is the sum of what sits beneath it, so an expense account under an asset group would produce a figure that means nothing.

System accounts

Every organization needs a skeleton of group accounts identified by a stable key in source_ref, with source: "system". An org created in this app is seeded automatically; any other org (orgs live in the auth service and are shared across apps) is seeded with POST /api/v1/accounts/seed (below) before its first account is created:

KeyName
ASSETSAssets
CURRENT_ASSETSCurrent Assets
CASHCash and Cash Equivalents
RECEIVABLESAccounts Receivable
NON_CURRENT_ASSETSNon-Current Assets
LIABILITIESLiabilities
CURRENT_LIABILITIESCurrent Liabilities
PAYABLESAccounts Payable
NON_CURRENT_LIABILITIESNon-Current Liabilities
CLEARINGClearing Accounts
EQUITYEquity
INCOMEIncome
EXPENSESExpenses
DEPRECIATIONDepreciation and Amortisation

Reports resolve these by key, never by name — so renaming one is safe. Its type and source_ref are not editable, because changing either would misfile everything beneath it on a statement with nothing to indicate anything went wrong.

Find one with ?source=system&source_ref=CASH.


GET /api/v1/accounts

ParamNotes
typeOne of the five types
statusactive, retired or quarantined
parentDirect children of this id; parent=root for top level
is_grouptrue or false
sourcemanual, system, or an upstream system name
source_refExact upstream identifier
qSubstring match on name
fieldsComma-separated subset of fields
page, limitDefault 100 per page, max 500

Ordered by type in statement order, then source_ref, then name.

GET /api/v1/accounts/tree

The whole chart nested, in one call, with children on each node. Unpaginated — a partial tree is not a tree.

ParamNotes
statusDefaults to excluding quarantined
include_emptyfalse prunes groups with nothing beneath them

Use include_empty=false to see only what can actually be posted to; a new organization's chart is mostly empty seeded headings.

The response also carries account_types, the table above, so a caller reading the tree knows which way each balance runs without a second request.

GET /api/v1/accounts/:id

One account.

POST /api/v1/accounts

{
  "name": "HDFC Current A/c",
  "type": "asset",
  "parent": "a1b2c3d4e5f6",
  "source": "sap",
  "source_ref": "100200"
}
FieldRequired
nameyes
typeyes
parent, source, source_ref, is_group, descriptionno

Idempotent on (source, source_ref). Re-posting an account an importer already sent updates it and returns created: false, so a re-run does not double the chart.

POST /api/v1/accounts/seed

Seeds this org's system groups. No body. Needs accounts:create.

{ "data": { "created": ["ASSETS", "CURRENT_ASSETS", "..."], "existing": [] } }

Idempotent. It creates only the keys that are missing, including any added to the system list since the org was seeded, and never changes an existing group (a renamed group keeps its name). Returns 201 when something was created, 200 when nothing was. Safe to call before every import run.

PATCH /api/v1/accounts/:id

Send only what changes: name, type, parent, is_group, description, status.

is_group cannot be turned on for an account that already carries entries — they would be stranded on an account nothing may post to. Move them to a child first.

DELETE /api/v1/accounts/:id

Refused if the account carries entries or has children.

Retire instead. status: "retired" keeps an account out of the live UI while historical reports stay correct. quarantined excludes it from reports too, and blocks new entries.