Menu

Budgets

Named budget versions per fiscal year with one default, amounts per resultatkonto (3000-8999), month and dimension tag in the SIE sign (revenue negative), saved all or nothing, filled from last year's actuals or spread over the months. Budget against actual is on the reports page.

Endpoints


GET /api/v1/companies/:companyId/budgets

budgets.list · scope reports:read

List the budgets (named versions per fiscal year).

Returns the company's budgets, oldest first: each is one named version of a fiscal year's budget, and at most one per year is the default that reports read when no budget is named. Archived budgets are left out unless include_archived is true.

Use when: Before reading or changing a budget, to find its budget_id, or to see which version of a year is the default.

Don't use for: The amounts themselves (GET /budgets/{id}) or the comparison with the books (GET /reports/budget-vs-actual).

Pitfalls

  • A company without budgets gets an empty list.

Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no

Query parameters

NameTypeRequiredDescription
fiscal_period_idstringnoThe fiscal year (fiscal_period_id).
include_archivedboolean | "true" | "false"noAlso list archived budgets. Default false.

Response fields

NameType
budgets[].budget_idstring
budgets[].fiscal_period_idstring
budgets[].namestring
budgets[].is_defaultboolean
budgets[].archivedboolean
budgets[].archived_atstring | null
budgets[].created_atstring
budgets[].updated_atstring

Example response

{
  "data": {
    "budgets": [
      {
        "budget_id": "7c1d…",
        "fiscal_period_id": "a8f1…",
        "name": "Budget 2027",
        "is_default": true,
        "archived": false,
        "archived_at": null,
        "created_at": "2026-11-02T09:00:00Z",
        "updated_at": "2026-11-02T09:00:00Z"
      }
    ]
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

GET /api/v1/companies/:companyId/budgets/:id

budgets.get · scope reports:read

Read one budget and its amounts per account, month and dimension.

Returns the budget and its lines, ordered by account and month, paged (limit, offset; has_more and total_lines say whether more follow). A line is one amount for one resultatkonto, one month and one dimension tag ({} untagged).

Use when: To read or check the amounts of a budget, e.g. before changing them.

Don't use for: Budget against booked result (GET /reports/budget-vs-actual).

Pitfalls

  • Amounts are in the SIE sign, debit positive: a revenue budget on class 3 is NEGATIVE (-1200000 for 1,2 Mkr sales), a cost budget positive.
  • A month with no line has no budget (0).

Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no

Query parameters

NameTypeRequiredDescription
account_numberstringnoA resultatkonto, 3000-8999, as a STRING of 4 digits, never a number.
limitnumbernoLines per page, 1-1000 (default 1000).
offsetnumbernoLines to skip (default 0).

Response fields

NameType
budgetobject
linesobject[]
total_linesnumber
has_moreboolean

Example response

{
  "data": {
    "budget": {
      "budget_id": "7c1d…",
      "fiscal_period_id": "a8f1…",
      "name": "Budget 2027",
      "is_default": true,
      "archived": false,
      "archived_at": null,
      "created_at": "2026-11-02T09:00:00Z",
      "updated_at": "2026-11-02T09:00:00Z"
    },
    "lines": [
      {
        "budget_line_id": "0b3e…",
        "account_number": "3010",
        "month": "2027-01",
        "dimensions": {},
        "amount": -100000
      }
    ],
    "total_lines": 1,
    "has_more": false
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/budgets

budgets.create · scope bookkeeping:write

Create a budget for a fiscal year.

Creates an empty, named budget version for one fiscal year. The name is unique within the year. The first budget of a year becomes its default unless is_default is false; is_default true on a later one moves the default to it. Fill it with POST /budgets/{id}/lines, /copy-actuals or /spread. Dry-runnable.

Use when: Starting a year's budget, or a second version of it (a forecast, a scenario).

Don't use for: Changing an existing budget (PATCH /budgets/{id}).

Pitfalls

  • A name the year already has (case ignored) answers 409 BUDGET_NAME_TAKEN.
  • fiscal_period_id must be one of the company's fiscal years: 404 PERIOD_NOT_FOUND.

Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes · Stage for approval: yes (?stage=true)

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.
stagestringnotrue stages the write for approval instead of committing it: 202 with the pending operation to approve via POST /pending-operations/{id}/approve. Absent or false commits; other values are 400. A reason in the body (why you propose it, at most 500 characters) is shown to the approver as agent_reason and kept with the decision.

Request body

NameTypeRequiredDescription
fiscal_period_idstringyesThe fiscal year (fiscal_period_id).
namestringyesThe budget's name, unique in its fiscal year ("Budget 2027", "Prognos 2").
is_defaultbooleannoMake it the year's default budget, the one reports read. Default: true when the year has no default yet.

Response fields

NameType
budgetobject

Example request

{
  "fiscal_period_id": "a8f1…",
  "name": "Budget 2027"
}

Example response

{
  "data": {
    "budget": {
      "budget_id": "7c1d…",
      "fiscal_period_id": "a8f1…",
      "name": "Budget 2027",
      "is_default": true,
      "archived": false,
      "archived_at": null,
      "created_at": "2026-11-02T09:00:00Z",
      "updated_at": "2026-11-02T09:00:00Z"
    }
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/budgets/:id/copy-actuals

budgets.copy-actuals · scope bookkeeping:write

Fill a budget with last year's booked result, optionally scaled.

Reads the booked result per resultatkonto and month of the source year (default: the year before the budget's year), with the resultatrapport's exclusions (no bokslut entries), and writes it into the budget month by month: March to March, scaled by percent_change. Each copied account gets every month of the year, so copying again replaces the earlier copy; accounts without actuals are left alone. With dimensions, only the tagged actuals are copied, into lines with the same tag. Dry-runnable.

Use when: Starting a budget from last year's figures, e.g. "last year plus 5 %".

Don't use for: A yearly amount for one account (POST /spread).

Pitfalls

  • Without a previous fiscal year, name one with source_fiscal_period_id: 404 BUDGET_SOURCE_YEAR_NOT_FOUND.
  • Months are matched by calendar month; a source year that holds a month twice (18 months) gives its later one.

Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes · Stage for approval: yes (?stage=true)

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.
stagestringnotrue stages the write for approval instead of committing it: 202 with the pending operation to approve via POST /pending-operations/{id}/approve. Absent or false commits; other values are 400. A reason in the body (why you propose it, at most 500 characters) is shown to the approver as agent_reason and kept with the decision.

Request body

NameTypeRequiredDescription
source_fiscal_period_idstringnoThe fiscal year (fiscal_period_id).
percent_changenumbernoScale every month by this percentage (5 = +5 %). Default 0.
dimensionsobjectnoCopy only the actuals tagged with this bag, into lines tagged the same.
account_numbersstring[]noOnly these accounts. Default: every resultatkonto with actuals.

Response fields

NameType
budget_idstring
insertednumber
updatednumber
deletednumber
unchangednumber
source_fiscal_period_idstring
accountsstring[]

Example request

{
  "percent_change": 5
}

Example response

{
  "data": {
    "budget_id": "7c1d…",
    "inserted": 24,
    "updated": 0,
    "deleted": 0,
    "unchanged": 0,
    "source_fiscal_period_id": "5c2e…",
    "accounts": [
      "3010",
      "5010"
    ]
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/budgets/:id/import

budgets.import · scope bookkeeping:write

Import a budget file (SIE #PBUDGET, Excel or CSV) into a budget.

Reads a budget file into one budget, all cells or none. A SIE file contributes its #PBUDGET rows (the month's budget per account and object, SIE sign); the rows for months of the budget's fiscal year are read and the file's other years are counted as skipped. A spreadsheet (xlsx, xls, csv, ods) is read in one of two layouts found from its header row: long (konto, månad as YYYY-MM, belopp, optionally dim and kod, or a projekt or kostnadsställe column) or wide (konto and one column per month, headed 2027-01 or jan). Accounts go through the same account mapping as the SIE ledger import, against the company's chart of accounts; an account the chart lacks, a balance account or an object the dimension registry lacks is skipped and named in details.skipped, never guessed. Dry-runnable: the dry run is the preview (what would be read, what is skipped and why, the budget per class and the result).

Use when: Moving a budget from Fortnox (its SIE export carries #PBUDGET) or from a spreadsheet, before following it up with GET /reports/budget-vs-actual.

Don't use for: A few cells (POST /budgets/{id}/lines), or the bookkeeping itself (POST /imports/sie).

Pitfalls

  • Send the file bytes base64-encoded in content_base64, up to about 3 MB of file; the filename's extension decides SIE or spreadsheet.
  • A spreadsheet's sign is read from its class 3 rows unless sign is sent: revenue positive means revenue_positive. sign_suspect true in the answer means revenue reads as a cost or costs as revenue: check the sign before saving.
  • replace true removes the budget's cells the file leaves out; without it the file is written over the budget and other cells stay.
  • Nothing for the budget's year answers 400 BUDGET_IMPORT_NOTHING_TO_IMPORT with the skipped lists in details.

Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.

Request body

NameTypeRequiredDescription
sign"debit_positive" | "revenue_positive"nodebit_positive: the SIE sign (revenue negative). revenue_positive: as the resultatrapport (revenue positive, costs negative). Default: read from the file (revenue positive on class 3 means revenue_positive). Ignored for a SIE file.
replacebooleannotrue: the budget's amounts become the file's, and cells the file leaves out are removed. Default false: the file's cells are written over the budget and other cells stay.
filenamestringyesThe file name with its extension, e.g. "budget-2027.xlsx" or "fortnox.se".
content_base64stringyesThe file's bytes, base64-encoded.

Response fields

NameType
budget_idstring
insertednumber
updatednumber
deletednumber
unchangednumber
format"sie" | "long" | "wide"
sign"debit_positive" | "revenue_positive"
detected_sign"debit_positive" | "revenue_positive" | null
replaceboolean
rows_readnumber
cellsnumber
accountsnumber
mapped_accountsobject[]
skippedobject
file_yearsobject[]
class_totalsobject[]
resultnumber
sign_suspectboolean

Example request

{
  "filename": "budget-2027.xlsx",
  "content_base64": "UEsDBBQ…",
  "replace": true
}

Example response

{
  "data": {
    "budget_id": "7c1d…",
    "inserted": 240,
    "updated": 0,
    "deleted": 0,
    "unchanged": 0,
    "format": "wide",
    "sign": "revenue_positive",
    "detected_sign": "revenue_positive",
    "replace": true,
    "rows_read": 240,
    "cells": 240,
    "accounts": 20,
    "mapped_accounts": [],
    "skipped": {
      "accounts_not_in_chart": [],
      "balance_accounts": [],
      "unknown_objects": [],
      "rows": 0,
      "months_outside_year": 0,
      "unreadable": [],
      "unreadable_count": 0
    },
    "file_years": [],
    "class_totals": [
      {
        "account_class": 3,
        "amount": 1200000
      }
    ],
    "result": 150000,
    "sign_suspect": false
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/budgets/:id/lines

budgets.lines.upsert · scope bookkeeping:write

Save amounts in a budget (account, month, dimension cells).

Writes each cell: one resultatkonto (3000-8999), one month (YYYY-MM) and one dimension tag ({} or omitted for untagged). A cell that exists gets the new amount, a new one is added, and amount 0 removes the cell. Every cell is saved or none is. The answer counts inserted, updated, deleted and unchanged cells. Dry-runnable.

Use when: Entering or adjusting budget amounts by hand, or loading a budget prepared elsewhere.

Don't use for: Last year's figures (POST /copy-actuals) or a yearly amount spread over the months (POST /spread).

Pitfalls

  • Amounts are in the SIE sign, debit positive: a revenue budget on class 3 is NEGATIVE (-1200000 for 1,2 Mkr sales), a cost budget positive.
  • The account must be in the chart of accounts: 404 BUDGET_ACCOUNT_NOT_FOUND lists the missing ones.
  • Months must lie in the budget's fiscal year: 400 BUDGET_MONTH_OUTSIDE_YEAR.
  • One cell once per call: 400 BUDGET_LINES_DUPLICATE.
  • An archived budget is read-only: 409 BUDGET_ARCHIVED.

Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes · Stage for approval: yes (?stage=true)

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.
stagestringnotrue stages the write for approval instead of committing it: 202 with the pending operation to approve via POST /pending-operations/{id}/approve. Absent or false commits; other values are 400. A reason in the body (why you propose it, at most 500 characters) is shown to the approver as agent_reason and kept with the decision.

Request body

NameTypeRequiredDescription
linesobject[]yesThe cells to save; every cell is written or none is.

Response fields

NameType
budget_idstring
insertednumber
updatednumber
deletednumber
unchangednumber

Example request

{
  "lines": [
    {
      "account_number": "3010",
      "month": "2027-01",
      "amount": -100000
    },
    {
      "account_number": "5010",
      "month": "2027-01",
      "dimensions": {
        "6": "P001"
      },
      "amount": 12000
    }
  ]
}

Example response

{
  "data": {
    "budget_id": "7c1d…",
    "inserted": 2,
    "updated": 0,
    "deleted": 0,
    "unchanged": 0
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

POST /api/v1/companies/:companyId/budgets/:id/spread

budgets.spread · scope bookkeeping:write

Spread a yearly amount over the months of a budget.

Writes one account's yearly amount into every month of the budget's fiscal year: evenly (method even), or in proportion to the account's booked months last year (method last_year, so a seasonal account keeps its season). Each month is whole öre and the last month takes the remainder, so the months add up to the amount exactly. A last year that sums to zero falls back to even (fell_back_to_even). Dry-runnable: the preview shows the months.

Use when: The budget for an account is known per year ("3010: 1,2 Mkr").

Don't use for: Different amounts per month (POST /lines).

Pitfalls

  • Amounts are in the SIE sign, debit positive: a revenue budget on class 3 is NEGATIVE (-1200000 for 1,2 Mkr sales), a cost budget positive.
  • The account's earlier months in the budget (same tag) are replaced.

Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes · Stage for approval: yes (?stage=true)

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.
stagestringnotrue stages the write for approval instead of committing it: 202 with the pending operation to approve via POST /pending-operations/{id}/approve. Absent or false commits; other values are 400. A reason in the body (why you propose it, at most 500 characters) is shown to the approver as agent_reason and kept with the decision.

Request body

NameTypeRequiredDescription
account_numberstringyesA resultatkonto, 3000-8999, as a STRING of 4 digits, never a number.
amountnumberyesThe yearly amount, SIE sign (revenue negative).
method"even" | "last_year"noeven: the same every month. last_year: in proportion to last year's months on the account. Default even.
dimensionsobjectnoDimension tag of the lines (and of the actuals the profile reads).
source_fiscal_period_idstringnoThe fiscal year (fiscal_period_id).

Response fields

NameType
budget_idstring
insertednumber
updatednumber
deletednumber
unchangednumber
account_numberstring
method"even" | "last_year"
fell_back_to_evenboolean
monthsobject[]

Example request

{
  "account_number": "3010",
  "amount": -1200000,
  "method": "last_year"
}

Example response

{
  "data": {
    "budget_id": "7c1d…",
    "inserted": 12,
    "updated": 0,
    "deleted": 0,
    "unchanged": 0,
    "account_number": "3010",
    "method": "last_year",
    "fell_back_to_even": false,
    "months": [
      {
        "month": "2027-01",
        "amount": -80000
      }
    ]
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

PATCH /api/v1/companies/:companyId/budgets/:id

budgets.update · scope bookkeeping:write

Rename a budget, make it the default, or archive or restore it.

Sparse update: name, is_default (true moves the year's default to this budget), archived (true makes it read-only and never the default; false restores it). The fiscal year of a budget never changes. Dry-runnable.

Use when: Choosing which version a year's reports read, renaming one, or putting an old version aside.

Don't use for: Changing amounts (POST /budgets/{id}/lines).

Pitfalls

  • Send at least one of name, is_default, archived.
  • An archived budget cannot be the default: 409 BUDGET_ARCHIVED.

Risk: low · Idempotent: yes · Reversible: yes · Dry-run supported: yes · Stage for approval: yes (?stage=true)

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.
stagestringnotrue stages the write for approval instead of committing it: 202 with the pending operation to approve via POST /pending-operations/{id}/approve. Absent or false commits; other values are 400. A reason in the body (why you propose it, at most 500 characters) is shown to the approver as agent_reason and kept with the decision.

Request body

NameTypeRequiredDescription
namestringnoThe budget's name, unique in its fiscal year ("Budget 2027", "Prognos 2").
is_defaultbooleannotrue makes it the year's default (the previous default stops being one).
archivedbooleannotrue archives it (read-only, never the default); false restores it.

Response fields

NameType
budgetobject

Example request

{
  "is_default": true
}

Example response

{
  "data": {
    "budget": {
      "budget_id": "7c1d…",
      "fiscal_period_id": "a8f1…",
      "name": "Budget 2027",
      "is_default": true,
      "archived": false,
      "archived_at": null,
      "created_at": "2026-11-02T09:00:00Z",
      "updated_at": "2026-11-02T09:00:00Z"
    }
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}

DELETE /api/v1/companies/:companyId/budgets/:id

budgets.delete · scope bookkeeping:write

Delete a budget and its amounts.

Removes the budget and every amount in it. A budget is planning data, not räkenskapsinformation (BFL 7 kap.), so it can be deleted at any time; the audit log keeps who did it. Archiving keeps it instead (PATCH archived=true). Dry-runnable: the preview counts the lines.

Use when: A budget version is no longer wanted.

Don't use for: Putting a version aside but keeping it (PATCH archived=true).

Pitfalls

  • A budget of another company, or one already deleted, answers 404 BUDGET_NOT_FOUND.

Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: yes · Stage for approval: yes (?stage=true)

Query parameters

NameTypeRequiredDescription
dry_runstringnotrue (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits.
stagestringnotrue stages the write for approval instead of committing it: 202 with the pending operation to approve via POST /pending-operations/{id}/approve. Absent or false commits; other values are 400. A reason in the body (why you propose it, at most 500 characters) is shown to the approver as agent_reason and kept with the decision.

Response fields

NameType
deletedtrue
budget_idstring

Example response

{
  "data": {
    "deleted": true,
    "budget_id": "7c1d…"
  },
  "meta": {
    "request_id": "req_…",
    "api_version": "2026-05-12"
  }
}