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.
| Type | Normal balance | Statement |
|---|---|---|
asset | debit | balance_sheet |
liability | credit | balance_sheet |
equity | credit | balance_sheet |
income | credit | income_statement |
expense | debit | income_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:
| Key | Name |
|---|---|
ASSETS | Assets |
CURRENT_ASSETS | Current Assets |
CASH | Cash and Cash Equivalents |
RECEIVABLES | Accounts Receivable |
NON_CURRENT_ASSETS | Non-Current Assets |
LIABILITIES | Liabilities |
CURRENT_LIABILITIES | Current Liabilities |
PAYABLES | Accounts Payable |
NON_CURRENT_LIABILITIES | Non-Current Liabilities |
CLEARING | Clearing Accounts |
EQUITY | Equity |
INCOME | Income |
EXPENSES | Expenses |
DEPRECIATION | Depreciation 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
| Param | Notes |
|---|---|
type | One of the five types |
status | active, retired or quarantined |
parent | Direct children of this id; parent=root for top level |
is_group | true or false |
source | manual, system, or an upstream system name |
source_ref | Exact upstream identifier |
q | Substring match on name |
fields | Comma-separated subset of fields |
page, limit | Default 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.
| Param | Notes |
|---|---|
status | Defaults to excluding quarantined |
include_empty | false 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"
}
| Field | Required |
|---|---|
name | yes |
type | yes |
parent, source, source_ref, is_group, description | no |
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.