Accountflow Developer next

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

  1. Create the company in Accountflow (https://next.accountflow.com), as usual.
  2. 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 409 with reason: company_not_api_managed. The check covers the year you are importing into as well: a year whose ledger was fetched from another accounting system is refused with the same 409, even if the company is now bound to Api.
  3. Create an API client with the imports:write scope (see the quickstart). Add ledger:read if 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.next.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:

  1. POST /v1/companies/{companyId}/imports/<kind> with up to 10 000 lines (and at most 8 MiB of body) and an Idempotency-Key header. More than that is 413 payload_too_large: split the data into several requests.
  2. The request is validated as a whole. Malformed input (a missing year, a period outside 1–12) is 400 validation_failed; well-formed lines that break a rule answer 422 unprocessable with an errors list of {field, message, index} (index 0-based into the array, up to 100 items) — and nothing is accepted.
  3. You get 202 Accepted with an import: id, status: "queued", jobId.
  4. Poll GET /v1/companies/{companyId}/imports/{importId} until status is succeeded or failed — or subscribe to the job.succeeded / job.failed webhook events for the jobId. GET /v1/jobs lists the jobs your client submitted, if you lose an id.
{
  "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
}

Amounts are decimal strings ("-1250.00", at most four decimals) — that is how the spec documents them; JSON numbers are accepted as well. Dates are ISO (2025-03-14).

Ids, keys and account codes (id, key, parentKey, itemKey, matchKey, accountCode, subLedgerKey, …) are yours, but they travel in paths and internal keys, so they may not contain :, /, {, } or whitespace, and they are bounded in length; a violation is 422 with the line in errors.

Reading back what you pushed

The read side of the API was designed from the ERP domain and uses its own names. The correspondence is exact:

You send (import) You read back On
accountCode accountNumber GET …/general-ledger/lines, GET …/accounts
accountDescription name GET …/accounts
accountCategory category GET …/accounts
id (a general-ledger line) sourceLineId (lineId is Accountflow's own id for the same line) GET …/general-ledger/lines
transactionNo voucherNo GET …/general-ledger/lines
transactionCurrencyCode transactionCurrency GET …/general-ledger/lines

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.next.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"] } ] }

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.