Menu

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

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

NameTypeDescription
service_accounts[].idstring
service_accounts[].handlestring (optional)Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is.
service_accounts[].company_idstring
service_accounts[].namestringShown as the actor of every write the account makes (behandlingshistorik, the voucher).
service_accounts[].sponsor_user_idstringThe 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_bystring
service_accounts[].created_atstring
service_accounts[].revoked_atstring | null
service_accounts[].revoked_bystring | 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

NameTypeDescription
idstring
handlestring (optional)Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is.
company_idstring
namestringShown as the actor of every write the account makes (behandlingshistorik, the voucher).
sponsor_user_idstringThe 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_bystring
created_atstring
revoked_atstring | null
revoked_bystring | null
keysobject[]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

NameTypeRequiredDescription
namestringyesUnique among the company's live service accounts; leading and trailing spaces are dropped.
sponsor_user_idstringnoThe sponsor: an owner or admin member of the company. Defaults to the user of the calling key.

Response fields

NameTypeDescription
idstring
handlestring (optional)Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is.
company_idstring
namestringShown as the actor of every write the account makes (behandlingshistorik, the voucher).
sponsor_user_idstringThe 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_bystring
created_atstring
revoked_atstring | null
revoked_bystring | 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

NameTypeRequiredDescription
namestringnoA 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")[]yesWhat 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"nolive (default) or test: a test key forces dry-run on every write.
read_onlybooleannotrue: the key may only read in the company, whatever its scopes. Default false.
acknowledge_sodbooleannoRequired true when scopes combine a staging write scope with pending_operations:approve (segregation of duties): otherwise 409 API_KEY_SOD_CONFLICT.

Response fields

NameTypeDescription
idstring
handlestring (optional)Typed handle of this API key: key_ plus its id as 32 lowercase hex. Accepted wherever the id is.
namestring
key_prefixstringThe first characters of the key, for recognising it. The key itself is shown once, when minted.
scopesstring[]
mode"live" | "test"
access"read_write" | "read_only"read_only: only read scopes apply in the company, whatever scopes the key holds.
created_atstring
last_used_atstring | null
revoked_atstring | null
service_account_idstring
keystringThe 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

NameTypeDescription
idstring
handlestring (optional)Typed handle of this API key: key_ plus its id as 32 lowercase hex. Accepted wherever the id is.
namestring
key_prefixstringThe first characters of the key, for recognising it. The key itself is shown once, when minted.
scopesstring[]
mode"live" | "test"
access"read_write" | "read_only"read_only: only read scopes apply in the company, whatever scopes the key holds.
created_atstring
last_used_atstring | null
revoked_atstring | 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

NameTypeDescription
idstring
handlestring (optional)Typed handle of this service account: svc_ plus its id as 32 lowercase hex. Accepted wherever the id is.
company_idstring
namestringShown as the actor of every write the account makes (behandlingshistorik, the voucher).
sponsor_user_idstringThe 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_bystring
created_atstring
revoked_atstring | null
revoked_bystring | 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"
  }
}