Accountflow Developer lab

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://lab.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.
  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.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:

  1. POST /v1/companies/{companyId}/imports/<kind> with up to 10 000 lines and an Idempotency-Key header. More lines than that is 413 payload_too_large: split the data into several requests.
  2. The request is validated as a whole. Structural problems answer 422 with an errors list of {index, field, message} (up to 100) 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.
{
  "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") 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"] } ] }

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.