Trial balance
One month of debit and credit movement per account, written and read back. This is the data the reports read: a month written here is the month the statements, the drill and the dashboard show.
Writing a trial balance
The endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/organisations/{organisation}/trial-balances |
Writes the month and recalculates |
| POST | /api/v1/organisations/{organisation}/trial-balances/validate |
The same checks; nothing is written |
Needs a bearer token, a credential issued for that company, and the credential's
trial_balance.write scope.
The body
{
"declarations": {
"source_reference": "netsuite-monthly",
"period": "2026-09",
"value_basis": "company_base",
"rate_direction": "document_to_base",
"base_currency": "SGD",
"mode": "replace_period"
},
"records": [
{ "account_code": "4000", "debit": 0, "credit": 12500.00 },
{ "account_code": "1100", "debit": 12500.00, "credit": 0 },
{ "account_code": "5000", "account_name": "Cost of sales", "debit": 800.00, "credit": 0 }
]
}
Declarations
| Declaration | Required | Value |
|---|---|---|
source_reference | yes | Your own id for the feed, 1–64 characters. |
period | yes | The month, YYYY-MM. YYYY-MM-DD is
read as that month. |
value_basis | yes | company_base for a trial balance — the
amounts are already in the reporting currency. |
rate_direction | yes | document_to_base or
base_to_document; it applies to any rate a record states. |
base_currency | yes | The company's reporting currency. A different one is
refused with base_currency_disagrees. |
scenario | no | Actual (default), Budget or Forecast. |
mode | no | replace_period (default), replace_all
or merge. |
switch_source | no | true states that this source takes the
month over from the one holding it. Defaults to false. |
webhook_url | no | Called once when the request finishes or fails. |
The fields
| Field | Type | Required | What it means |
|---|---|---|---|
account_code | string | yes | The code in this company's accounts. It is the record's identity. |
account_name | string | no | Kept beside the code as a reader sees it. |
debit | number | no | The month's debit movement. Absent means zero. |
credit | number | no | The month's credit movement. Absent means zero. |
A code the company does not hold is refused with account_not_found and the answer names the
accounts kind. A trial balance never creates an account.
What it does
- A month is sent whole. Under
replace_periodthe month is replaced for that source: a code absent from the payload has no rows in that month afterwards. There is no partial trial balance and no line to delete one by one. - One month can have one owner. If another source already holds the month, the request is
refused with
month_held_by_another_sourceunless it statesswitch_source: true, which removes the other source's rows for that month and records the removal — the request reportsresult.switched_fromandresult.rows_removed. - The reports then recalculate. The answer is immediate, with
recalculation: "pending"; the request's own URL reportsdone, orfailedwith the step that failed. No caller waits inside an HTTP request for a recalculation that can take minutes. - A locked or out-of-window month is refused with
period_lockedorperiod_outside_window. The window is the edition's actual window, so it boundsActualmonths only: a budget or a forecast is a month beyond the actuals and is accepted there. - A month is written against a scenario.
Actualis the ledger, stored under the API's own source.BudgetandForecastare the plan: the rows are stored as the plan rows the report screen's own plan entry writes, for the scenario declared, and rebuilt with the plan recalculation. Only the accounts the payload states are replaced, because a plan is entered account by account and a payload that names three of the chart's accounts says nothing about the rest.
What it answers
{
"id": 87, "kind": "trial_balance", "source_reference": "netsuite-monthly",
"period": "2026-09", "status": "written", "records_received": 412,
"counts": { "created": 412, "updated": 0, "unchanged": 0, "rejected": 0 },
"recalculation": "pending", "recalculation_step": null, "recalculation_failure": null,
"result": {},
"written_at": "2026-10-23T06:14:02+00:00",
"links": { "self": "…/requests/87", "results": "…/requests/87/results" }
}
A month sent twice with the same content reports unchanged for every record and moves no
figure.
Refusals
| Code | When |
|---|---|
account_not_found | The code is not in this company's accounts |
month_held_by_another_source | Another source holds the month and the request does not switch source |
period_locked | The month is behind a period lock |
period_outside_window | An Actual month is outside the edition's
allowed window |
base_currency_disagrees | The declared currency is not the company's |
invalid_value | scenario is none of Actual,
Budget, Forecast; or a plan is sent to a level the group does not accept plan
input at |
duplicate_record_identity | Two records state the same
account_code |
A refused record never stops the rest: the request is written, that record is reported
rejected with its own code in the results.
Reading the trial balance
One month of the ledger, account by account, with the movement and the closing balance the reports are built from.
The endpoint
| Method | Path | Answers |
|---|---|---|
| GET | /api/v1/organisations/{organisation}/trial-balances |
Every account's figures for the month |
Needs a bearer token, a credential issued for that company, and the trial_balance.read scope.
The parameters
| Parameter | What it does |
|---|---|
period | The month. Either spelling is accepted, Jul-2026 or
2026-07; the answer states the label, Mon-YYYY. Absent, the company's
reporting month is answered. |
number_of_months | How many trailing months to answer, 1 to 120. Default
1. |
currency | The currency to present in, three letters. Default: the company's own. A currency the group holds no rate for is refused rather than answered untranslated. |
scenario | Actual, Budget or Forecast.
Default Actual. A plan scenario answers the plan rows of that scenario, which is what a
Budget or Forecast write stores. |
account_code | One account, instead of the whole ledger. A code that is not
there answers an empty list, not 404: a filter narrows and does not assert. |
What it answers
GET /api/v1/organisations/149/trial-balances?period=Jul-2026
{
"data": [
{
"code": "SA-1000", "name": "Bank", "period": "Jul-2026", "scenario": "Actual",
"debit": 125000.0, "credit": 40000.0,
"debit_ytd": 700000.0, "credit_ytd": 650000.0,
"movement": 85000.0, "balance": 250000.0, "average": 250000.0
}
],
"page": 1, "per_page": 50, "total": 33, "has_more": false
}
| Field | What it says |
|---|---|
code, name | The account, as the accounts hold it. |
period, scenario | Which month and which version of the figures. |
debit, credit | The month's movement on each side, as the ledger recorded them: both are unsigned, so the side is not lost in the sign. |
debit_ytd, credit_ytd | The year-to-date movement on each side. |
movement | The month's movement on the account's own normal side: positive when it moved the way the account usually moves. It is the figure a profit and loss line reads. |
balance | The closing balance at the month end, on the same normal side: the position the account stands at, and the figure a balance sheet line reads. |
average | The mid-month balance, balance - movement / 2, which a
profit and loss balance is translated at with the average rate. |
Amounts are answered at stored precision in the currency asked for; ask for the company's own currency and
nothing is translated. Every amount belongs to the month in period on the same row - a row is
one account in one month, and the months run oldest first.
The signs are the ones the statements answer in. A bank account holding a debit answers a
positive balance here and a positive line on the balance sheet, so the two add up without a
caller flipping one of them; a liability carries a positive figure on its own side too. To read the ledger
the way the accounting equation does, use debit and credit, which are always
unsigned.