Importing data
Push a company's books through the API: accounts, master data, balances, lines, open items.
If your company's books live in your own system rather than in an ERP Accountflow fetches from, you push them through the API. This page takes you from an empty company to a loaded ledger. Removing or fixing what you pushed is on the corrections page.
Before you start
- Create the company in Accountflow (https://lab.accountflow.com), as usual.
- Connect it to the Api accounting system. In the company's accounting-system
settings choose Api. There is nothing to fill in: choosing it is what tells
Accountflow that this company's data arrives through the API. Until you do, every
import answers
409withreason: company_not_api_managed. - Create an API client with the
imports:writescope (see the quickstart). Addledger:readif the same client should read the data back. Imports are for system clients only.
Check where you stand at any time:
curl -s https://api.lab.accountflow.com/v1/companies/$COMPANY/imports/readiness \
-H "Authorization: Bearer $TOKEN" | jq
{
"apiManaged": true,
"accountingSystem": "Api",
"currentYear": 2025,
"years": [
{ "year": 2025, "current": true, "accounts": 0, "mappedAccounts": 0,
"openingBalances": 0, "generalLedgerLines": 0, "lastImportAt": null }
],
"dimensions": 0,
"subLedgers": 0,
"blockers": ["no_accounts"],
"warnings": [],
"nextStep": "import_accounts"
}
blockers is what stops you importing; nextStep is the one thing to do next.
How every import works
Every import is the same four steps, whatever you push:
POST /v1/companies/{companyId}/imports/<kind>with up to 10 000 lines and anIdempotency-Keyheader. More lines than that is413 payload_too_large: split the data into several requests.- The request is validated as a whole. Structural problems answer
422with anerrorslist of{index, field, message}(up to 100) and nothing is accepted. - You get
202 Acceptedwith an import:id,status: "queued",jobId. - Poll
GET /v1/companies/{companyId}/imports/{importId}untilstatusissucceededorfailed— or subscribe to thejob.succeeded/job.failedwebhook events for thejobId.
{
"id": "0b8f…", "kind": "general_ledger_lines", "year": 2025, "mode": "merge",
"status": "succeeded", "jobId": "6d1c…", "datasetId": "0b8f…", "lineCount": 9800,
"rowsDiscovered": 9800, "rowsValid": 9795, "rowsFailed": 5,
"rowsInserted": 9700, "rowsUpdated": 95, "rowsDeleted": null,
"sampleErrors": ["line 12: missing required field: amount"],
"failureReason": null
}
- One import per kind and company runs at a time. A second one answers
409withreason: job_in_flightand thejobIdto wait for. Send large data as a sequence: wait for one import to finish, then send the next. - Retrying is safe. The same
Idempotency-Keyreturns the same import; it never creates a second one. succeededcan still carry failed lines.rowsFailedandsampleErrors(up to 100,line N: message, N counting from 1) tell you which lines were skipped. Fix and re-send just those; lines are merged byid, so re-sending the rest is harmless.datasetIdis stamped on every row the import touched. Quote it to support.
Amounts are decimal strings ("-1250.00") or JSON numbers, with at most four
decimals. Dates are ISO (2025-03-14).
The order to push in
| # | Kind | Endpoint | Scope |
|---|---|---|---|
| 1 | Chart of accounts | POST …/imports/accounts |
one accounting year |
| 2 | Dimension values (optional) | POST …/imports/dimensions |
the company |
| 3 | Sub-ledgers (optional) | POST …/imports/sub-ledgers |
the company |
| 4 | Opening balances | POST …/imports/opening-balances |
one accounting year |
| 5 | General-ledger lines | POST …/imports/general-ledger-lines |
one accounting year |
| 6 | Open items (optional) | POST …/imports/open-items |
customers or suppliers |
Accounts come first because everything else hangs on them: opening balances and
general-ledger lines that name an account code missing from that year's chart are
refused (422, reason: unknown_account_codes, with the codes). Dimensions and
sub-ledgers are optional up front — a line may name a key you have not pushed, and it
is created as a plain value — but pushing them first gives them proper names,
types and hierarchy.
1. Chart of accounts
curl -s -X POST https://api.lab.accountflow.com/v1/companies/$COMPANY/imports/accounts \
-H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" -d '{
"year": 2025,
"accounts": [
{ "accountCode": "1920", "accountDescription": "Bankinnskudd",
"accountCategory": "Balance", "currencyCode": "NOK" },
{ "accountCode": "3000", "accountDescription": "Salgsinntekt",
"accountCategory": "Result" }
]
}'
An existing accountCode is updated in place. Its standard-account mapping is never
touched by an import — mapping is done in Accountflow (or by auto-map), and
readiness.warnings shows unmapped_accounts while some remain. hidden: true
hides an account in the application; omit hidden to leave the flag as it is. A new
accounting year gets its own chart: push the accounts for each year you load.
2–3. Dimensions and sub-ledgers
{ "dimensions": [
{ "key": "DEPT", "value": "Departments", "type": "department" },
{ "key": "DEPT-10", "value": "Sales", "type": "department", "parentKey": "DEPT" } ] }
{ "subLedgers": [ { "key": "C-1001", "value": "Kunde AS", "type": "customer" } ] }
Keys are yours, stable, and unique per company (not per year). Read them back with
GET …/dimensions and GET …/sub-ledgers.
4. Opening balances
{ "year": 2025, "lines": [
{ "id": "OB-1920", "accountCode": "1920", "amount": "125000.00" },
{ "id": "OB-1500", "accountCode": "1500", "amount": "48200.00",
"subLedgerKeys": ["C-1001"] } ] }
id is your own stable id for the row: re-sending it updates the row. The trial
balance's opening column follows.
5. General-ledger lines
{ "year": 2025, "lines": [
{ "id": "INV-2025-000123-1", "accountCode": "3000", "amount": "-1250.00",
"period": 3, "transactionId": "V-2025-0456", "transactionNo": "456",
"description": "Salg mars", "transactionDate": "2025-03-14",
"taxCode": "3", "taxAmount": "-312.50", "taxPercentage": "25",
"dimensionKeys": ["DEPT-10"], "subLedgerKeys": ["C-1001"] } ] }
idis the line's identity — yours, stable, and never containing:. Re-send a line with the sameidand it is updated in place, with the previous version kept in history. Send a newidand you get a new line; there is no fuzzy matching.- You supply
period(1–12) andyear. Nothing is derived from dates. transactionIdgroups the lines of one voucher;transactionNois the voucher number accountants see.- Lines are merged, never replaced: a line you do not re-send stays. To remove one, delete it.
- When you read lines back (
GET …/general-ledger/lines), your id issourceLineId.lineIdis Accountflow's own identifier for the same line.
6. Open items
Customer or supplier open items, with their matches, in the import contract's shape:
{ "subLedgerType": "customer", "mode": "incremental", "items": [
{ "itemKey": "inv-1001", "subLedgerKey": "C-1001", "subLedgerType": "customer",
"accountCode": "1500", "documentId": "inv-1001", "entryCategory": "invoice",
"documentDate": "2025-03-14", "dueDate": "2025-04-13",
"amount": "1250.00", "amountOpen": "250.00", "settlementStatus": "partial",
"matches": [ { "matchKey": "pay-77", "matchDate": "2025-04-01", "amount": "1000.00" } ] } ] }
incremental touches only what you send. full, full_refresh and baseline are
snapshots: items and matches absent from the snapshot are retired. Unlike the other
kinds, one invalid item fails the whole import — nothing is half-applied.
What importing does not do
Pushing data never triggers anything else: no reconciliation run, no VAT import, no notifications. Those happen on their own schedules or when someone starts them in Accountflow.