Menu
Cookbooks
API reference
- Overview
- Companies
- Customers
- Invoices
- Articles
- Suppliers
- Supplier invoices
- Supplier payment files
- Expense claims
- Transactions
- Reconciliation
- Bank accounts
- Journal entries
- Voucher gap explanations
- Fiscal periods
- Accounts
- Fixed assets
- Documents
- Inbox items
- Dimensions
- Employees
- Salary runs
- Reports
- Imports
- Compliance check
- Skatteverket
- Peppol
- Webhooks
- Operations
- Health
Bank accounts
The company's bank accounts (cash accounts, whose ids filter transactions) and the bank connections that sync them.
Endpoints
GET/api/v1/companies/:companyId/bank-connections: List PSD2 bank connections with sync freshness and consent expiry.GET/api/v1/companies/:companyId/cash-accounts: List bank/cash accounts with the bank-reported balance.POST/api/v1/companies/:companyId/bank-connections/:connectionId/sync: Sync one bank connection now instead of waiting for the nightly run.POST/api/v1/companies/:companyId/cash-accounts: Create a bank account by hand (no bank connection), with the payee details invoices print.POST/api/v1/companies/:companyId/cash-accounts/:id/set-primary: Make a bank account the company's primary.PATCH/api/v1/companies/:companyId/cash-accounts/:id: Edit a bank account: verifikationsserie, payee details, name, or turn it on/off.PUT/api/v1/companies/:companyId/cash-accounts/payee-defaults: Choose which bank account invoices in a currency tell the customer to pay to.
GET /api/v1/companies/:companyId/bank-connections
bank-connections.list · scope companies:read
List PSD2 bank connections with sync freshness and consent expiry.
Returns every bank connection for the company with its status, last successful sync (last_synced_at), consent expiry (consent_expires) and any user-facing error message. Connections sync automatically once a day server-side; this endpoint tells you whether that is still happening.
Use when: You need to verify bank data is current before building on it (liquidity, reconciliation, reports), or to detect a dead connection that needs BankID re-authorisation.
Don't use for: Fetching transactions (use /transactions) or account balances (use /cash-accounts). Triggering a sync: not available on this surface; syncing is automatic.
Pitfalls
- last_synced_at is null until the first sync completes (about a minute after connecting); it does NOT mean the connection is broken.
- A connection can hold status=active with a stale last_synced_at (older than ~36 hours): treat the data as suspect, but do NOT assume re-authorisation fixes it. Common causes are a lapsed subscription (this endpoint then answers with a capability error) or every account deselected in settings.
- status=expired means the PSD2 consent is dead: only the user can fix it, with BankID in a browser.
- error_message is Swedish and user-facing: show it verbatim rather than translating.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Response fields
| Name | Type |
|---|---|
bank_connections[].connection_id | string |
bank_connections[].bank | string | null |
bank_connections[].status | "pending" | "pending_selection" | "active" | "expired" | "error" |
bank_connections[].since | string |
bank_connections[].last_synced_at | string | null |
bank_connections[].consent_expires | string | null |
bank_connections[].error_message | string | null |
Example response
{
"data": {
"bank_connections": [
{
"connection_id": "4f6c…",
"bank": "Swedbank",
"status": "active",
"since": "2026-08-01T00:00:00Z",
"last_synced_at": "2026-08-31T05:04:12Z",
"consent_expires": "2026-11-01T00:00:00Z",
"error_message": null
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
GET /api/v1/companies/:companyId/cash-accounts
cash-accounts.list · scope transactions:read
List bank/cash accounts with the bank-reported balance.
Returns the company's cash accounts (bank accounts, kassa) with their BAS ledger mapping and, for PSD2-connected accounts, the balance the bank itself reported at the last sync: balance (booked), available_balance, and balance_updated_at (when it was fetched). Pass ?enabled_only=true to return only accounts that sync.
Use when: You need the current bank balance per account (e.g. a covering decision before a payment run), or cash_account_id values to filter transaction listings.
Don't use for: The bookkept 19xx balance: use the trial-balance or balance-sheet reports. The two legitimately differ (pending bookings, timing).
Pitfalls
- balance/available_balance are what the BANK reported, refreshed at most every 12h (PSD2 quota): check balance_updated_at before treating them as current.
- balance is null for manual and SIE-imported accounts, and for PSD2 accounts that have not completed a sync since connecting.
- available_balance is null when the bank reports no available balance type; that does not mean 0.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
enabled_only | "true" | "false" | no | true returns only enabled accounts. Default: all accounts. |
Response fields
| Name | Type |
|---|---|
cash_accounts[].cash_account_id | string |
cash_accounts[].ledger_account | string |
cash_accounts[].name | string | null |
cash_accounts[].currency | string |
cash_accounts[].iban | string | null |
cash_accounts[].is_primary | boolean |
cash_accounts[].enabled | boolean |
cash_accounts[].source | "enable_banking" | "manual" | "sie_import" |
cash_accounts[].balance | number | null |
cash_accounts[].available_balance | number | null |
cash_accounts[].balance_updated_at | string | null |
Example response
{
"data": {
"cash_accounts": [
{
"cash_account_id": "ca_…",
"ledger_account": "1930",
"name": "Företagskonto",
"currency": "SEK",
"iban": "SE4550000000058398257466",
"is_primary": true,
"enabled": true,
"source": "enable_banking",
"balance": 125430.5,
"available_balance": 123930.5,
"balance_updated_at": "2026-09-01T05:12:44.000Z"
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/bank-connections/:connectionId/sync
bank-connections.sync · scope transactions:write
Sync one bank connection now instead of waiting for the nightly run.
Fetches new transactions and balances for one PSD2 bank connection right away. The window is chosen server-side: the last 7 days, widened to cover any gap since last_synced_at, capped at 90 days. Returns how many transactions were imported and the new last_synced_at. A connection synced within the last 15 minutes is refused with 429 BANK_SYNC_COOLDOWN and next_allowed_at: the data is already fresh. Not dry-runnable: the bank call itself is the side effect.
Use when: GET /bank-connections shows a stale last_synced_at on an active connection and you need current bank data before building on it (liquidity, reconciliation, a report), or the user asks for the latest transactions now.
Don't use for: Polling. Connections sync every night on their own; call this once when freshness matters, then read /transactions. Fixing a dead connection: status=expired needs BankID in a browser, not a sync.
Pitfalls
- Idempotency-Key is optional here. If you send one, use a fresh key per attempt: a cooldown answer is never cached, but a completed sync is, and replaying it fetches nothing new.
- 429 BANK_SYNC_COOLDOWN follows a recent successful sync OR a recent attempt that failed (the 15-minute lease is taken before the bank is called, on every instance). Compare last_synced_at from GET /bank-connections: if it is fresh, use the data you have; if it is still stale, the previous attempt failed, so retry once after next_allowed_at (Retry-After is set).
- 429 BANK_RATE_LIMITED is the BANK limiting the consent (PSD2 banks allow only a few unattended fetches per day), not this API. The connection stays valid: do not renew it. Wait until next_allowed_at (Retry-After is set); it is our cooldown, not a reset time confirmed by the bank.
- 409 BANK_SESSION_EXPIRED means the bank reported the consent dead during the sync; the connection is now status=expired. Hand the user the connect link; no API call revives it.
- imported: 0 is normal on a quiet account. Banks report with up to 48 hours of delay, so today's transactions often arrive tomorrow.
- Costs one Enable Banking call per enabled account: 403 CAPABILITY_BLOCKED when the company has no bank_sync entitlement.
Risk: low · Idempotent: no · Reversible: no · Dry-run supported: no
Response fields
| Name | Type |
|---|---|
connection_id | string |
bank | string | null |
imported | number |
duplicates | number |
from_date | string |
to_date | string |
last_synced_at | string |
Example response
{
"data": {
"connection_id": "4f6c…",
"bank": "Swedbank",
"imported": 3,
"duplicates": 12,
"from_date": "2026-08-26",
"to_date": "2026-09-02",
"last_synced_at": "2026-09-02T09:14:03Z"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/cash-accounts
cash-accounts.create · scope companies:write
Create a bank account by hand (no bank connection), with the payee details invoices print.
Adds a manual bank account (cash_accounts, source manual) in a currency, on the next free BAS 19xx ledger account for that currency unless ledger_account (1920-1999) is given, and adds that account to the chart if missing. payee holds what customer invoices print (bankgiro, IBAN, ...); invoice_payee defaults to true. A later bank connection with the same IBAN takes this row over in place. Owner/admin only. Idempotent. Dry-runnable.
Use when: The company has a bank account that is not connected through the bank integration (a savings account, a currency account, a bank without PSD2) and it should appear in Konton, the booking flows or on invoices.
Don't use for: Connecting a bank (the bank connection flow creates its own accounts), changing an existing account (PATCH /cash-accounts/{id}) or choosing which account invoices print by default (PUT /cash-accounts/payee-defaults).
Pitfalls
- An IBAN another account of the company already carries returns 409 CASH_ACCOUNT_IBAN_DUPLICATE: one physical account must exist once.
- A ledger_account another cash account holds returns 409 CASH_ACCOUNT_LEDGER_TAKEN; omit it to get the next free one.
- ledger_account is a STRING in 1920-1999 ("1931"), never a number, and never a till (1910-1919) or a PSP clearing account.
- Owner or admin only: a member key gets 403 FORBIDDEN.
- Creating an account does not make it the default payee: set that with PUT /cash-accounts/payee-defaults.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Request body
| Name | Type | Required |
|---|---|---|
name | string | yes |
currency | "SEK" | "EUR" | "USD" | "GBP" | "NOK" | "DKK" | "CHF" | yes |
ledger_account | string | no |
invoice_payee | boolean | no |
payee | object | no |
Response fields
| Name | Type | Description |
|---|---|---|
cash_account_id | string | |
ledger_account | string | BAS 19xx account the bank account books on, as a string. |
name | string | null | |
currency | string | |
iban | string | null | The bank identity IBAN (written by the bank sync). |
source | "enable_banking" | "manual" | "sie_import" | |
bank_connected | boolean | True when a bank connection holds the account. |
enabled | boolean | |
is_primary | boolean | |
voucher_series | string | null | Verifikationsserie override; null follows the per-source default. |
invoice_payee | boolean | Whether the account may be printed as payee on customer invoices. |
payee | object |
Example request
{
"name": "Sparkonto",
"currency": "SEK",
"payee": {
"bank_name": "SEB",
"bankgiro": "5050-1234"
}
}
Example response
{
"data": {
"cash_account_id": "7f3a…",
"ledger_account": "1931",
"name": "Sparkonto",
"currency": "SEK",
"iban": "SE4550000000058398257466",
"source": "manual",
"bank_connected": false,
"enabled": true,
"is_primary": false,
"voucher_series": null,
"invoice_payee": true,
"payee": {
"bank_name": "SEB",
"clearing_number": null,
"account_number": null,
"bankgiro": "5050-1234",
"plusgiro": null,
"swish": null,
"iban": "SE4550000000058398257466",
"bic": "ESSESESS",
"bank_code": null,
"foreign_account_number": null
}
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/cash-accounts/:id/set-primary
cash-accounts.set-primary · scope companies:write
Make a bank account the company's primary.
The primary is where bookings land when nothing else says which bank account they belong to: the skattekonto counter leg and transactions with no cash account. It must be an enabled SEK giro or bank account (BAS 1920-1999). The flag moves in one transaction and the change is logged with the acting user. Only bookings made afterwards follow the new primary; nothing posted changes. Owner/admin only. Idempotent. Dry-runnable.
Use when: The company's main business account is not the one marked primary (typically the seeded 1930).
Don't use for: Choosing which account invoices print (PUT /cash-accounts/payee-defaults) or moving transactions between accounts.
Pitfalls
- A disabled, non-SEK or non-bank account (till, PSP clearing) returns 400 CASH_ACCOUNT_PRIMARY_INELIGIBLE with details.reason.
- Owner or admin only: a member key gets 403 FORBIDDEN.
- Takes no body; the account id is in the path.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Response fields
| Name | Type | Description |
|---|---|---|
cash_account_id | string | |
ledger_account | string | BAS 19xx account the bank account books on, as a string. |
name | string | null | |
currency | string | |
iban | string | null | The bank identity IBAN (written by the bank sync). |
source | "enable_banking" | "manual" | "sie_import" | |
bank_connected | boolean | True when a bank connection holds the account. |
enabled | boolean | |
is_primary | boolean | |
voucher_series | string | null | Verifikationsserie override; null follows the per-source default. |
invoice_payee | boolean | Whether the account may be printed as payee on customer invoices. |
payee | object |
Example response
{
"data": {
"cash_account_id": "7f3a…",
"ledger_account": "1931",
"name": "Sparkonto",
"currency": "SEK",
"iban": "SE4550000000058398257466",
"source": "manual",
"bank_connected": false,
"enabled": true,
"is_primary": true,
"voucher_series": null,
"invoice_payee": true,
"payee": {
"bank_name": "SEB",
"clearing_number": null,
"account_number": null,
"bankgiro": "5050-1234",
"plusgiro": null,
"swish": null,
"iban": "SE4550000000058398257466",
"bic": "ESSESESS",
"bank_code": null,
"foreign_account_number": null
}
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
PATCH /api/v1/companies/:companyId/cash-accounts/:id
cash-accounts.update · scope companies:write
Edit a bank account: verifikationsserie, payee details, name, or turn it on/off.
Sparse update of one cash account. voucher_series (one letter A-Z, null clears) sets the verifikationsserie for entries booked from the account. The payee fields (bank_name, clearing_number, account_number, bankgiro, plusgiro, swish, iban, bic, bank_code, foreign_account_number), name and invoice_payee decide what customer invoices print; "" or null clears a field. enabled=false hides an account no bank connection holds from Konton and the booking flows. The ledger account and the primary flag are not editable here. Idempotent. Dry-runnable.
Use when: The company changes bank details customers pay to, wants its own voucher series per bank account, or stops using a manually added account.
Don't use for: Making an account the primary (POST /cash-accounts/{id}/set-primary), choosing the default payee per currency (PUT /cash-accounts/payee-defaults) or moving a transaction to another account.
Pitfalls
- Payee fields, name, invoice_payee and enabled are owner/admin only (403 FORBIDDEN); voucher_series alone is open to any writer.
- Payee fields on a PSP clearing account or a till return 400 INVOICE_PAYEE_ACCOUNT_INVALID: only 1920-1999 bank accounts print on invoices.
- enabled on an account a bank connection holds returns 409 CASH_ACCOUNT_ENABLED_BANK_MANAGED; disabling the primary returns 400 CASH_ACCOUNT_DISABLE_PRIMARY, and one with unbooked transactions 400 CASH_ACCOUNT_DISABLE_UNRESOLVED.
- An iban another account already carries returns 409 CASH_ACCOUNT_IBAN_DUPLICATE.
- Changing voucher_series only affects entries booked afterwards; nothing posted is renumbered.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Request body
| Name | Type | Required |
|---|---|---|
bank_name | string | null | no |
clearing_number | string | null | "" | no |
account_number | string | null | "" | no |
bankgiro | string | null | "" | no |
plusgiro | string | null | "" | no |
swish | string | null | no |
iban | string | null | "" | no |
bic | string | null | "" | no |
bank_code | string | null | "" | no |
foreign_account_number | string | null | "" | no |
voucher_series | string | null | no |
name | string | null | no |
invoice_payee | boolean | no |
enabled | boolean | no |
Response fields
| Name | Type | Description |
|---|---|---|
cash_account_id | string | |
ledger_account | string | BAS 19xx account the bank account books on, as a string. |
name | string | null | |
currency | string | |
iban | string | null | The bank identity IBAN (written by the bank sync). |
source | "enable_banking" | "manual" | "sie_import" | |
bank_connected | boolean | True when a bank connection holds the account. |
enabled | boolean | |
is_primary | boolean | |
voucher_series | string | null | Verifikationsserie override; null follows the per-source default. |
invoice_payee | boolean | Whether the account may be printed as payee on customer invoices. |
payee | object |
Example request
{
"bankgiro": "5050-1234",
"invoice_payee": true
}
Example response
{
"data": {
"cash_account_id": "7f3a…",
"ledger_account": "1931",
"name": "Sparkonto",
"currency": "SEK",
"iban": "SE4550000000058398257466",
"source": "manual",
"bank_connected": false,
"enabled": true,
"is_primary": false,
"voucher_series": null,
"invoice_payee": true,
"payee": {
"bank_name": "SEB",
"clearing_number": null,
"account_number": null,
"bankgiro": "5050-1234",
"plusgiro": null,
"swish": null,
"iban": "SE4550000000058398257466",
"bic": "ESSESESS",
"bank_code": null,
"foreign_account_number": null
}
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
PUT /api/v1/companies/:companyId/cash-accounts/payee-defaults
cash-accounts.set-payee-default · scope companies:write
Choose which bank account invoices in a currency tell the customer to pay to.
Sets (or clears with cash_account_id null) the default payee account for one currency: every new invoice in that currency prints this account's payment details unless the invoice picks another. The account must be a bank account (1920-1999), enabled, flagged invoice_payee, and carry what the currency needs (an IBAN for anything but SEK). Answers every per-currency default after the change. Owner/admin only. Idempotent. Dry-runnable.
Use when: The company wants EUR invoices paid to its EUR account, or changes which SEK account customers pay to.
Don't use for: Editing the bank details themselves (PATCH /cash-accounts/{id}) or the primary account (set-primary).
Pitfalls
- An account that cannot print for the currency returns 400 INVOICE_PAYEE_ACCOUNT_INVALID with details.reason (not_bank_account, disabled, not_payee, unusable_for_currency).
- Invoices already sent keep the payment details they were sent with.
- Owner or admin only: a member key gets 403 FORBIDDEN.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: yes
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | string | no | true (any case) previews the write without committing it, like the X-Dry-Run: true header. Any other value commits. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
currency | "SEK" | "EUR" | "USD" | "GBP" | "NOK" | "DKK" | "CHF" | yes | Invoice currency, e.g. "SEK" or "EUR". |
cash_account_id | string | null | yes | The cash account id (cash_account_id from GET /cash-accounts), not its ledger account. |
Response fields
| Name | Type |
|---|---|
defaults[].currency | string |
defaults[].cash_account_id | string |
Example request
{
"currency": "EUR",
"cash_account_id": "7f3a…"
}
Example response
{
"data": {
"defaults": [
{
"currency": "EUR",
"cash_account_id": "7f3a…"
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}