Menu
Cookbooks
API reference
- Overview
- Companies
- Capabilities
- Measures
- Packages
- Unexplained
- Liquidity forecast
- Portfolio
- Customers
- Invoices
- Collections
- Articles
- Set-offs
- Suppliers
- Supplier invoices
- Supplier payment files
- Bank payments
- Expense claims
- Transactions
- Reconciliation
- Bank accounts
- POS sales
- Webshop orders
- Journal entries
- Voucher gap explanations
- Fiscal periods
- Accounts
- Accrual schedules
- Agreements and commitments
- Tax loss carry-forwards
- Fixed assets
- Documents
- Signing requests
- Company registers
- Capital events
- Owner tax
- Inbox items
- Dimensions
- Employees
- Salary runs
- Reports
- Budgets
- Imports
- Compliance check
- Skatteverket
- Peppol
- Webhooks
- Operations
- Pending operations
- Health
- API key
- Service accounts
Service accounts
An integration's own principal with a human sponsor: create, list, read and revoke service accounts, and mint or revoke their keys. Writes by their keys are recorded as the service account, acting for the sponsor.
Endpoints
GET/api/v1/companies/:companyId/service-accounts: List the company's service accounts.GET/api/v1/companies/:companyId/service-accounts/:id: Show one service account with its keys.POST/api/v1/companies/:companyId/service-accounts: Create a service account with a human sponsor.POST/api/v1/companies/:companyId/service-accounts/:id/keys: Mint an API key for a service account; the secret is shown once.POST/api/v1/companies/:companyId/service-accounts/:id/keys/:keyId/revoke: Revoke one key of a service account.POST/api/v1/companies/:companyId/service-accounts/:id/revoke: Revoke a service account and all its keys.
GET /api/v1/companies/:companyId/service-accounts
service-accounts.list · scope service_accounts:manage
List the company's service accounts.
Returns every service account of the company, live ones first, then the revoked ones, each with its sponsor and status. Keys are listed per account on GET /service-accounts/{id}.
Use when: You manage the non-human principals of a company: to find the account an integration runs as, or to check that none is suspended.
Don't use for: Listing the API keys of people (not exposed over the API). Reading a key's secret: it is returned once, when the key is minted.
Pitfalls
- Needs a person's key with service_accounts:manage, held by an owner or admin of the company. A service account's own key is refused (403 SERVICE_ACCOUNT_KEY_NOT_PERMITTED).
- status suspended means the sponsor is no longer an owner or admin: the account's keys answer 403 SERVICE_ACCOUNT_SUSPENDED until the role is restored.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Response fields
| Name | Type | Description |
|---|---|---|
service_accounts[].id | string | |
service_accounts[].handle | string (optional) | Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is. |
service_accounts[].company_id | string | |
service_accounts[].name | string | Shown as the actor of every write the account makes (behandlingshistorik, the voucher). |
service_accounts[].sponsor_user_id | string | The person who answers for the account: an owner or admin of the company. Its keys act as this user for access, and every row they write carries this user. |
service_accounts[].status | "active" | "suspended" | "revoked" | active: its keys work. suspended: the sponsor is no longer an owner or admin (or the company is archived), so its keys answer 403 SERVICE_ACCOUNT_SUSPENDED until that changes. revoked: final, its keys are revoked. |
service_accounts[].created_by | string | |
service_accounts[].created_at | string | |
service_accounts[].revoked_at | string | null | |
service_accounts[].revoked_by | string | null |
Example response
{
"data": {
"service_accounts": [
{
"id": "5b6c1d2e-0000-4000-8000-0000000000a1",
"company_id": "8fd5b1f4-0000-4000-8000-000000000001",
"name": "Nattlig fakturasynk",
"sponsor_user_id": "3f2a9c10-0000-4000-8000-000000000002",
"status": "active",
"created_by": "3f2a9c10-0000-4000-8000-000000000002",
"created_at": "2026-10-16T08:00:00Z",
"revoked_at": null,
"revoked_by": null
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
GET /api/v1/companies/:companyId/service-accounts/:id
service-accounts.get · scope service_accounts:manage
Show one service account with its keys.
Returns the service account, its sponsor and status, and every key it has (live and revoked, newest first) with prefix, scopes, mode, access and last use. A key's secret is never returned after it was minted.
Use when: You check which keys an integration's service account has before rotating or revoking one.
Don't use for: Recovering a lost key: mint a new one and revoke the old one.
Pitfalls
- An id that is not one of this company's service accounts answers 404 SERVICE_ACCOUNT_NOT_FOUND.
- Needs a person's key with service_accounts:manage, held by an owner or admin of the company.
Risk: low · Idempotent: yes · Reversible: no · Dry-run supported: no
Response fields
| Name | Type | Description |
|---|---|---|
id | string | |
handle | string (optional) | Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is. |
company_id | string | |
name | string | Shown as the actor of every write the account makes (behandlingshistorik, the voucher). |
sponsor_user_id | string | The person who answers for the account: an owner or admin of the company. Its keys act as this user for access, and every row they write carries this user. |
status | "active" | "suspended" | "revoked" | active: its keys work. suspended: the sponsor is no longer an owner or admin (or the company is archived), so its keys answer 403 SERVICE_ACCOUNT_SUSPENDED until that changes. revoked: final, its keys are revoked. |
created_by | string | |
created_at | string | |
revoked_at | string | null | |
revoked_by | string | null | |
keys | object[] | Every key of the account, live and revoked, newest first. Never the secrets. |
Example response
{
"data": {
"id": "5b6c1d2e-0000-4000-8000-0000000000a1",
"company_id": "8fd5b1f4-0000-4000-8000-000000000001",
"name": "Nattlig fakturasynk",
"sponsor_user_id": "3f2a9c10-0000-4000-8000-000000000002",
"status": "active",
"created_by": "3f2a9c10-0000-4000-8000-000000000002",
"created_at": "2026-10-16T08:00:00Z",
"revoked_at": null,
"revoked_by": null,
"keys": [
{
"id": "7a1e0c55-0000-4000-8000-0000000000b1",
"name": "Nattlig fakturasynk",
"key_prefix": "gnubok_sk_Xb3kQ9aZ",
"scopes": [
"invoices:read",
"invoices:write"
],
"mode": "live",
"access": "read_write",
"created_at": "2026-10-16T08:01:00Z",
"last_used_at": null,
"revoked_at": null
}
]
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/service-accounts
service-accounts.create · scope service_accounts:manage
Create a service account with a human sponsor.
Creates a service account in the company: a non-human principal for an integration or agent, with a sponsor who answers for it. The sponsor must be an owner or admin member of the company and defaults to the user of the calling key. The account has no key yet: mint one with POST /service-accounts/{id}/keys. Everything its keys write is recorded with the actor service_account (the account's name), acting for the sponsor.
Use when: You connect an integration or agent that should act under its own name in the books rather than as the person who set it up.
Don't use for: Giving a person access (invite them to the company instead). Changing the sponsor or the name: revoke the account and create a new one.
Pitfalls
- A service account can never do more than its sponsor: when the sponsor stops being an owner or admin, every key of the account is refused (403 SERVICE_ACCOUNT_SUSPENDED) until the role is restored.
- Names are unique among the company's live accounts (409 SERVICE_ACCOUNT_NAME_TAKEN); a revoked account frees its name.
- At most 25 live service accounts per company (409 SERVICE_ACCOUNT_LIMIT).
- Not available with a test key (403 TEST_KEY_WRITE_BLOCKED): there is no dry-run.
Risk: medium · Idempotent: yes · Reversible: yes · Dry-run supported: no
Request body
| Name | Type | Required | Description |
|---|---|---|---|
name | string | yes | Unique among the company's live service accounts; leading and trailing spaces are dropped. |
sponsor_user_id | string | no | The sponsor: an owner or admin member of the company. Defaults to the user of the calling key. |
Response fields
| Name | Type | Description |
|---|---|---|
id | string | |
handle | string (optional) | Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is. |
company_id | string | |
name | string | Shown as the actor of every write the account makes (behandlingshistorik, the voucher). |
sponsor_user_id | string | The person who answers for the account: an owner or admin of the company. Its keys act as this user for access, and every row they write carries this user. |
status | "active" | "suspended" | "revoked" | active: its keys work. suspended: the sponsor is no longer an owner or admin (or the company is archived), so its keys answer 403 SERVICE_ACCOUNT_SUSPENDED until that changes. revoked: final, its keys are revoked. |
created_by | string | |
created_at | string | |
revoked_at | string | null | |
revoked_by | string | null |
Example request
{
"name": "Nattlig fakturasynk"
}
Example response
{
"data": {
"id": "5b6c1d2e-0000-4000-8000-0000000000a1",
"company_id": "8fd5b1f4-0000-4000-8000-000000000001",
"name": "Nattlig fakturasynk",
"sponsor_user_id": "3f2a9c10-0000-4000-8000-000000000002",
"status": "active",
"created_by": "3f2a9c10-0000-4000-8000-000000000002",
"created_at": "2026-10-16T08:00:00Z",
"revoked_at": null,
"revoked_by": null
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/service-accounts/:id/keys
service-accounts.keys.create · scope service_accounts:manage
Mint an API key for a service account; the secret is shown once.
Creates a gnubok_sk_ key that acts as the service account: it reaches only the account's company, acts with the access of the account's sponsor, and everything it writes is recorded with the actor service_account (the account's name), acting for the sponsor. The response carries the secret in key, exactly once. Its scopes must be ones the calling key holds itself; service_accounts:manage is never granted, so a service account cannot mint keys or create accounts.
Use when: You set up the integration that runs as the service account, or rotate its key (mint the new one, switch, revoke the old one).
Don't use for: A key for a person (create it in Settings > API & MCP). Recovering a lost secret: mint a new key and revoke the old one.
Pitfalls
- The secret is returned exactly once. Idempotency-Key is not honoured: a repeated call mints a second key, so check GET /service-accounts/{id} and revoke any extra.
- Each requested scope must be held by the calling key (403 SERVICE_ACCOUNT_SCOPE_NOT_HELD, details.not_held lists the rest).
- A staging write scope together with pending_operations:approve needs acknowledge_sod: true (409 API_KEY_SOD_CONFLICT otherwise).
- At most 5 live keys per account (409 SERVICE_ACCOUNT_KEY_LIMIT). A revoked account takes no new key (409 SERVICE_ACCOUNT_REVOKED); a suspended one neither (403 SERVICE_ACCOUNT_SUSPENDED).
- When the sponsor stops being an owner or admin, the key is refused at every call with 403 SERVICE_ACCOUNT_SUSPENDED until the role is restored.
Risk: high · Idempotent: no · Reversible: yes · Dry-run supported: no
Request body
| Name | Type | Required | Description |
|---|---|---|---|
name | string | no | A label for this key. Defaults to the account's name. |
scopes | ("transactions:read" | "transactions:write" | "customers:read" | "customers:write" | "articles:read" | "articles:write" | "invoices:read" | "invoices:draft" | "invoices:write" | "suppliers:read" | "suppliers:register" | "suppliers:write" | "reports:read" | "dimensions:read" | "bookkeeping:write" | "payroll:read" | "payroll:write" | "companies:read" | "companies:write" | "events:read" | "webhooks:manage" | "operations:read" | "documents:read" | "documents:write" | "compliance:read" | "skatteverket:write" | "agent:read" | "agent:write" | "pending_operations:read" | "pending_operations:approve" | "reconciliation:read" | "reconciliation:write" | "reconciliation:signoff" | "forecast:write")[] | yes | What the key may do. Each must be a scope the calling key holds itself; service_accounts:manage is never granted to a service account. |
mode | "live" | "test" | no | live (default) or test: a test key forces dry-run on every write. |
read_only | boolean | no | true: the key may only read in the company, whatever its scopes. Default false. |
acknowledge_sod | boolean | no | Required true when scopes combine a staging write scope with pending_operations:approve (segregation of duties): otherwise 409 API_KEY_SOD_CONFLICT. |
Response fields
| Name | Type | Description |
|---|---|---|
id | string | |
handle | string (optional) | Typed handle of this API key: key_ plus its id as 32 lowercase hex. Accepted wherever the id is. |
name | string | |
key_prefix | string | The first characters of the key, for recognising it. The key itself is shown once, when minted. |
scopes | string[] | |
mode | "live" | "test" | |
access | "read_write" | "read_only" | read_only: only read scopes apply in the company, whatever scopes the key holds. |
created_at | string | |
last_used_at | string | null | |
revoked_at | string | null | |
service_account_id | string | |
key | string | The secret (gnubok_sk_...), returned exactly once: store it now. Only its SHA-256 is kept. |
Example request
{
"scopes": [
"invoices:read",
"invoices:write"
]
}
Example response
{
"data": {
"id": "7a1e0c55-0000-4000-8000-0000000000b1",
"name": "Nattlig fakturasynk",
"key_prefix": "gnubok_sk_Xb3kQ9aZ",
"scopes": [
"invoices:read",
"invoices:write"
],
"mode": "live",
"access": "read_write",
"created_at": "2026-10-16T08:01:00Z",
"last_used_at": null,
"revoked_at": null,
"service_account_id": "5b6c1d2e-0000-4000-8000-0000000000a1",
"key": "gnubok_sk_Xb3kQ9aZ…"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/service-accounts/:id/keys/:keyId/revoke
service-accounts.keys.revoke · scope service_accounts:manage
Revoke one key of a service account.
Revokes the key: the next request with it is refused. The account and its other keys are untouched. Revoking a revoked key answers it unchanged.
Use when: You rotate a service account's key (after switching the integration to the new one) or a single key may have leaked.
Don't use for: Stopping the whole integration at once: revoke the account (POST /service-accounts/{id}/revoke).
Pitfalls
- Final: a revoked key cannot be restored. Mint a new one.
- A key that is not one of this account's keys in this company answers 404 SERVICE_ACCOUNT_KEY_NOT_FOUND.
Risk: medium · Idempotent: yes · Reversible: no · Dry-run supported: no
Response fields
| Name | Type | Description |
|---|---|---|
id | string | |
handle | string (optional) | Typed handle of this API key: key_ plus its id as 32 lowercase hex. Accepted wherever the id is. |
name | string | |
key_prefix | string | The first characters of the key, for recognising it. The key itself is shown once, when minted. |
scopes | string[] | |
mode | "live" | "test" | |
access | "read_write" | "read_only" | read_only: only read scopes apply in the company, whatever scopes the key holds. |
created_at | string | |
last_used_at | string | null | |
revoked_at | string | null |
Example response
{
"data": {
"id": "7a1e0c55-0000-4000-8000-0000000000b1",
"name": "Nattlig fakturasynk",
"key_prefix": "gnubok_sk_Xb3kQ9aZ",
"scopes": [
"invoices:read",
"invoices:write"
],
"mode": "live",
"access": "read_write",
"created_at": "2026-10-16T08:01:00Z",
"last_used_at": null,
"revoked_at": "2026-11-02T09:30:00Z"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}
POST /api/v1/companies/:companyId/service-accounts/:id/revoke
service-accounts.revoke · scope service_accounts:manage
Revoke a service account and all its keys.
Revokes the service account and, in the same transaction, every key it has: the next request with any of them is refused. The account is never deleted: it stays listed with status revoked, so the processing history keeps naming it. Revoking a revoked account answers it unchanged.
Use when: An integration is retired, or a key may have leaked and you want everything the account can do stopped at once.
Don't use for: Rotating one key: mint a new key, then revoke the old one with POST /service-accounts/{id}/keys/{keyId}/revoke.
Pitfalls
- Final: a revoked account cannot get new keys or be restored. Create a new account (the name is free again).
- Writes the account made stay as they are; they keep naming the account and its sponsor.
Risk: high · Idempotent: yes · Reversible: no · Dry-run supported: no
Response fields
| Name | Type | Description |
|---|---|---|
id | string | |
handle | string (optional) | Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is. |
company_id | string | |
name | string | Shown as the actor of every write the account makes (behandlingshistorik, the voucher). |
sponsor_user_id | string | The person who answers for the account: an owner or admin of the company. Its keys act as this user for access, and every row they write carries this user. |
status | "active" | "suspended" | "revoked" | active: its keys work. suspended: the sponsor is no longer an owner or admin (or the company is archived), so its keys answer 403 SERVICE_ACCOUNT_SUSPENDED until that changes. revoked: final, its keys are revoked. |
created_by | string | |
created_at | string | |
revoked_at | string | null | |
revoked_by | string | null |
Example response
{
"data": {
"id": "5b6c1d2e-0000-4000-8000-0000000000a1",
"company_id": "8fd5b1f4-0000-4000-8000-000000000001",
"name": "Nattlig fakturasynk",
"sponsor_user_id": "3f2a9c10-0000-4000-8000-000000000002",
"status": "revoked",
"created_by": "3f2a9c10-0000-4000-8000-000000000002",
"created_at": "2026-10-16T08:00:00Z",
"revoked_at": "2026-11-02T09:30:00Z",
"revoked_by": "3f2a9c10-0000-4000-8000-000000000002"
},
"meta": {
"request_id": "req_…",
"api_version": "2026-05-12"
}
}