API reference

What the Fortuna API accepts and returns: how callers authenticate, who may call what, the response envelope and error statuses, paging, filtering and sorting, rate limiting, and the 31 endpoint groups.

This page is the map to the Fortuna API. It explains the rules every endpoint shares, then lists the endpoint groups. The endpoints themselves, with their parameters and schemas, are in the API explorer, which renders the API’s OpenAPI document.

The list of endpoints follows that OpenAPI document, which is generated from the code. The System Requirements Document describes the intended surface and differs from the real one in places (it lists PUT /api/me, which does not exist), so when the two disagree this page follows the OpenAPI document and the code. The counts below were taken from the document on fortuna-api’s main branch on 2026-10-13, the release v2.1.0 of the API.

Authentication

Every endpoint needs a valid bearer token, except the anonymous routes listed below. Send the token in the Authorization header as Bearer <token>. A missing, malformed, expired or wrongly signed token is answered with 401 Unauthorized.

In connected mode the token is a JWT issued by Heimdall, the identity provider Fortuna is registered with. The API validates it locally, without calling Heimdall: it checks the signature against the shared secret, the issuer, the audience and the expiry, with no allowance for clock skew. The secret, issuer and audience are the FORTUNA_AUTH_TOKEN_SECRET, FORTUNA_AUTH_TOKEN_ISSUER and FORTUNA_AUTH_TOKEN_AUDIENCE settings, which must match Heimdall’s; see Connecting to Heimdall. The token carries the user’s identifier and a numeric role, using Heimdall’s values: 1 for a system administrator, 2 for a scope administrator, 3 for a user.

Callers do not talk to Heimdall to sign in. They call Fortuna’s own /api/auth/* routes, and Fortuna forwards each exchange to Heimdall with its scope attached. The full sequence, including two-factor sign-in, is in Signing in through Heimdall. Because a token is never checked against Heimdall after it is issued, a token revoked there stays valid in Fortuna until it expires.

Offline: the local account

A desktop installation running offline has no Heimdall to reach. It has its own local account, created through POST /api/local-accounts (or its native equivalent) and signed in through POST /api/local-accounts/authenticate. The token it returns is local to the installation, and the account holds the user role. The native core offers the same routes in process through a C interface instead of HTTP, derived from the same OpenAPI document; some routes exist there but answer 501. How that works and what is missing is in the local account and what offline mode does not support yet.

Anonymous routes

The API denies by default: an authenticated caller is required unless a route is explicitly marked anonymous in the code. Thirteen routes are. The OpenAPI document does not mark them, so this table, checked against the controllers, is the place to find them.

RoutePurposeRate limited
GET /healthcheckLiveness: the service name and the contract version, no dependency checks.No
POST /api/auth/loginExchange an email and password for a token or a two-factor challenge.Yes
POST /api/auth/googleExchange a Google ID token for a token.Yes
POST /api/auth/2fa/verifyComplete a two-factor challenge.Yes
POST /api/auth/2fa/challenge/resendSend a new code for an outstanding challenge.Yes
POST /api/auth/password-recoveryAsk for password-recovery instructions.Yes
POST /api/auth/password-resetSet a new password with a reset token.Yes
POST /api/auth/registerRegister a connected identity.Yes
POST /api/auth/verify-emailVerify an address, or confirm a sign-up.Yes
POST /api/local-accountsCreate the desktop local account.No
POST /api/local-accounts/authenticateSign in to the local account.Yes
POST /api/local-accounts/recoverSpend a recovery code to regain access.Yes
POST /api/local-accounts/password-resetAlways refuses with 404: a local account has no password reset. With local authentication on, the message is PasswordResetUnavailable.No

The local-account routes answer 404 when local authentication is turned off. POST /api/local-accounts/password-reset answers 404 whether it is on or off (PasswordResetUnavailable when on). Regenerating recovery codes with POST /api/local-accounts/recovery-codes/regenerate for an account that is not a local account also answers 404 (LocalRecoveryCodeRegenerationMessages.LocalAccountOnly). The credential routes give one 401 for any rejected credential, so a response never reveals whether an account exists.

Authorization

Authentication says who the caller is; the role says what they may do. The routes fall into five groups. The count of each, taken from the code, adds up to the 145 operations in the document.

Who may callOperationsRoutes
Anyone13The anonymous routes.
Any signed-in caller10GET /api/currencies, GET /api/currencies/{code}, the /api/auth routes that act on the caller’s own session (Google sign-out, resend verification, the two-factor status, enable, confirm and disable, and two-factor recovery-code regeneration), and POST /api/local-accounts/recovery-codes/regenerate.
The user role118Everything that touches financial data: accounts, cards, investments, transactions, transfers, categories, reports, imports, exports, attachments, the audit trail and the caller’s own profile and consents. Two of these, POST /api/me/data-export and GET /api/me/data-export/{jobId}, also accept a system administrator.
Installation administrator2POST /api/exchange-rates (record a manual rate) and POST /api/exchange-rates/sync. A Heimdall system administrator, or the owner of an offline installation signed in with its local account.
System administrator2GET /healthcheck/detailed and DELETE /api/users/{id} (erase a user).

A caller with the wrong role gets 403 Forbidden. Three rules sit on top of the roles, from the authorization matrix in the System Requirements Document:

  • Nobody reads another user’s records. An administrator who targets a financial record is refused with 403, because administering an instance gives no access to what is in it. A user who names a record that belongs to someone else gets 404, the same answer as for a record that does not exist, so that no response reveals what another user holds.
  • Managing who may sign in is Heimdall’s job, not Fortuna’s. The only administrator route for users in Fortuna is erasure, DELETE /api/users/{id}.
  • A local account can do everything an account owner can, except use a network source (it has no network offline), so its imports are files only.

The response envelope

Responses from the API use one envelope. The native core uses the same one.

FieldTypeMeaning
dataanyThe result of the request.
errorslist of stringsWhy the request failed.
messageslist of stringsNotes about the outcome that are not errors, such as a confirmation on success.
successbooleanWhether the request succeeded. Read only.
timestampdate and timeWhen the response was made.

For example, a refused request:

{
  "data": null,
  "errors": ["PageSize must be at least 1."],
  "messages": [],
  "success": false,
  "timestamp": "2026-10-13T12:00:00Z"
}

A few responses are not wrapped in it: the two health routes return their own report, and the responses that are files (a data export, an attachment, a finished personal-data archive) return the file. A file comes back unwrapped only when the request succeeds and a file is there to return. When it does not, as when the export or personal-data archive is not ready yet or the request is refused, the response is the ordinary envelope: this is true of the export retrieval route (GET /api/exports/{id}), GET /api/me/data-export/{jobId} and the attachment download alike.

Errors

Handlers do not throw for expected failures. They return an error message, and the controller turns the message into a status through a map of its own, merged over a shared one. The result is a small, consistent set of statuses.

StatusWhen
400 Bad RequestA value is invalid. The message names the failing field.
401 UnauthorizedThe token is missing, invalid or expired, or credentials were rejected.
403 ForbiddenThe role is not allowed, a financial record was targeted by an administrator, or consent for external data processing is required.
404 Not FoundThe record does not exist, or belongs to someone else; also a local-account route when local authentication is off.
409 ConflictA conflict with existing data, such as hard-deleting a record that has not been soft-deleted first, or an exchange rate that is not available for a conversion.
422 Unprocessable EntityA required confirmation was left out, as on verify-email.
429 Too Many RequestsThe rate limit was reached, or Heimdall’s was (see Rate limiting).
503 Service UnavailableA dependency is down: file storage, or Heimdall. The message gives no upstream detail.

A request that is accepted for later work, such as a registration, a data export or an export job, is answered with 202 Accepted. The OpenAPI document lists only some of these statuses (most operations document only 200), so read the statuses above as the rules the code follows, not as a per-route contract. The calendar dates the API accepts are yyyy-MM-dd, and amounts are sent and returned as decimal strings, never as floating-point numbers.

Pagination, filtering and sorting

List endpoints take their paging, sorting and filters as query parameters.

ParameterMeaning
PageNumberWhich page, starting at 1.
PageSizeItems per page, at least 1. A larger value is capped at the instance’s maximum, 100 unless FORTUNA_PAGE_SIZE_MAX says otherwise.
SortByThe field to order by. Each endpoint accepts its own names and has its own default (accounts sort by Name, transaction search by OccurredOn). An unsupported name is a 400 that lists the valid ones.
Descendingtrue to reverse the order. The transaction search defaults to newest first.
IncludeDeletedInclude soft-deleted records, on the endpoints that offer it (accounts, cards, investments, recurring transactions, transactions and a transaction’s attachments).

Beyond these, each list has filters of its own, listed in the explorer. For example, accounts filter by Name, Institution, AccountType and CurrencyCode. The transaction search filters by From and To, account, card, category, tag, counterparty, Direction, a minimum and maximum amount, free Text and more, and can show figures in a DisplayCurrencyCode at a FigureDate.

A page comes back in the envelope, with the paging fields next to the usual ones:

FieldMeaning
dataThe items of this page.
pageNumber, pageSizeThe page that was returned.
totalItems, totalPagesThe size of the whole result.

The transaction search differs: its data is an object holding items, pageNumber, pageSize, totalItems, totalPages and totals, the sums over the whole matching set.

The reports have their own parameters as well: an aggregate takes a Dimension (period, category, account, card, counterparty or tag), a Granularity and a date range, and a drill-down takes the Key of the group to open; the explorer lists the rest. The explorer also lists PageNumber and PageSize on /api/reports/aggregate, /api/reports/drill-down, /api/reports/net-position and /api/projections/cash-flow and /api/projections/commitments, but the aggregate, net-position and projection outputs are not paged. Only the drill-down pages its transactions (TransactionDrillDownOutput carries pageNumber, pageSize, totalItems and totalPages).

Rate limiting

Ten anonymous routes are rate limited: the eight anonymous /api/auth routes and the local-account authenticate and recover routes. The limit is 10 requests per minute per client address, counted in a fixed one-minute window, with no queue. The eleventh request in a window is answered with 429 Too Many Requests.

  • The client address is the key. IPv6 addresses are grouped by their /64 prefix, so rotating addresses within one prefix does not escape the limit, and an IPv4 address mapped into IPv6 counts as the IPv4 address. Behind a reverse proxy, the address is the real client’s only if the proxy is configured as a trusted one.
  • Heimdall has its own limit, on the requests Fortuna forwards. Unless FORTUNA_HEIMDALL_CALLER_KEY is set, every user of one Fortuna instance shares Heimdall’s budget, so a busy instance can see 429 before an individual caller reaches ten. That 429 carries the message “Too many attempts. Wait a minute and try again.”
  • Authenticated routes have no limit in the API. Outbound calls to other services back off and respect those services’ own limits.

The proxy and caller-key settings are under Reverse proxy and client addresses on the Operations page.

Endpoint groups

The document groups its 145 operations over 109 paths into 31 tags. Where a group has both DELETE {id} and DELETE {id}/hard, the first is a soft delete and the second removes a record that is already soft-deleted.

TagOperationsWhat it covers
Accounts8Financial accounts: list, create, read, update, soft and hard delete, restore, and the computed balance.
Attachments3Read (download), delete and hard delete one attachment.
AuditEntries1The caller’s own audit trail, filterable by entity, operation, outcome and date.
Auth15Connected sign-in through Heimdall: login, Google, registration, email verification, password recovery and reset, and two-factor setup and verification.
Budgets6Budgets: list, create, read, update, delete, and consumption against the budget.
Categories8Categories: the usual lifecycle plus reassigning the records of one category to another.
Connections5Open-banking connections: list, create, read, reauthenticate, revoke.
ConnectionSynchronization1Start a synchronization of one connection.
Counterparties6Counterparties: list, create, update, delete, merge two, and a suggested category.
CreditCards8Credit cards: the usual lifecycle plus a card’s statements.
Currencies2The supported currencies, and one by code.
DataSources1List the data sources the API offers.
DetailedHealthCheck1Administrator-only health of the service and each dependency.
ExchangeRates3Convert amounts between currencies; record a manual rate; synchronize rates.
Exports2Request a data-set export and retrieve its result.
Goals6Goals: list, create, read, update, delete, and progress.
HealthCheck1Anonymous liveness.
ImportJobs4Import jobs: list, read, read the imported records, and retry.
Imports2Import an Excel workbook or a PDF statement (multipart upload).
InstallmentPlans4Create, read, delete and restore an installment plan.
Investments10Investments: the usual lifecycle, movements, and valuations (list and record).
LocalAccounts5The desktop local account: create, authenticate, recover, regenerate recovery codes, and a password reset that always refuses.
Me7The caller’s profile, processing consents, personal-data export, and self-erasure.
Projections2Cash-flow and committed-obligation projections.
RecurringTransactions6Recurring transactions: the usual lifecycle plus materializing due occurrences.
Reports4Aggregate, drill-down, net position, and a generic table query.
Statements3Read, close and settle a credit-card statement.
Tags4Tags: list, create, update, delete.
Transactions12Transactions: search and the usual lifecycle, attachments, tags, reconciliation, and restore.
Transfers4Create, read, delete and restore a transfer between accounts.
Users1Administrator erasure of a user.

A few things to know when reading the explorer:

  • The document describes a contract version of v1 (info.version), which is independent of the API’s release numbers. GET /healthcheck reports it.
  • It declares no servers; use the address of the instance you run.
  • Its single security scheme is a bearer JWT, “the Heimdall JWT bearer token”, but no operation says whether it applies. See Authorization and the anonymous routes above.

API explorer

Swagger UI over fortuna-api’s OpenAPI document — every endpoint and its schema.