> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leara.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Fortnox integration: architecture, data flows, and security

> Technical overview of the Leara Fortnox integration: OAuth 2.0 scopes, two-way data flows, real-time sync, reconciliation, rate limits, and known limitations.

Leara connects to Fortnox through Fortnox's official APIs and keeps data in sync in both directions: in real time via the Fortnox event stream, with an automatic reconciliation every 15 minutes as a safety net. Customers connect the integration themselves with OAuth 2.0 through the Fortnox login; Leara never handles the customer's Fortnox password.

## Summary

Leara has its own enriched data model. Product data, variants, product groups, and the customer register are owned by Leara and written to Fortnox. Bookkeeping, invoice numbers, and payment status are owned by Fortnox and mirrored to Leara. Fields that Fortnox does not support remain in Leara and are never overwritten by incoming Fortnox data.

The integration is built in Leara's own backend, with no intermediate iPaaS or third-party connector. Everything runs in the AWS region eu-north-1 (Stockholm).

## Architecture

The integration is part of Leara's backend and consists of four parts: a write queue to Fortnox, a real-time client for the Fortnox event service, a scheduled reconciliation, and a separate token service. The apps never talk directly to Fortnox.

<div className="leara-diagram my-6 w-full">
  <svg viewBox="0 0 960 440" width="100%" role="img" aria-label="Leara Fortnox architecture diagram">
    <defs>
      <marker id="fx-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
        <path d="M0,0 L10,5 L0,10 z" fill="currentColor" />
      </marker>
    </defs>

    <text x="95" y="80" textAnchor="middle" fontSize="15" fontWeight="600" fill="currentColor">Apps</text>

    <rect fill="none" x="20" y="150" width="150" height="44" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="95" y="177" textAnchor="middle" fontSize="14" fill="currentColor">Web app</text>

    <rect fill="none" x="20" y="210" width="150" height="44" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="95" y="237" textAnchor="middle" fontSize="14" fill="currentColor">iOS app</text>

    <rect fill="none" x="20" y="270" width="150" height="44" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="95" y="297" textAnchor="middle" fontSize="14" fill="currentColor">Android app</text>

    <line x1="172" y1="172" x2="238" y2="172" stroke="currentColor" strokeWidth="1.5" markerStart="url(#fx-arrow)" markerEnd="url(#fx-arrow)" />

    <line x1="172" y1="232" x2="238" y2="232" stroke="currentColor" strokeWidth="1.5" markerStart="url(#fx-arrow)" markerEnd="url(#fx-arrow)" />

    <line x1="172" y1="292" x2="238" y2="292" stroke="currentColor" strokeWidth="1.5" markerStart="url(#fx-arrow)" markerEnd="url(#fx-arrow)" />

    <rect fill="none" x="240" y="55" width="380" height="360" rx="14" stroke="currentColor" strokeWidth="1.5" />

    <text x="430" y="82" textAnchor="middle" fontSize="16" fontWeight="600" fill="currentColor">Leara backend</text>
    <text x="430" y="100" textAnchor="middle" fontSize="12" fill="currentColor" fillOpacity="0.65">AWS eu-north-1 (Stockholm)</text>

    <rect fill="none" x="270" y="115" width="320" height="40" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="430" y="140" textAnchor="middle" fontSize="14" fill="currentColor">Write queue</text>

    <rect fill="none" x="270" y="170" width="320" height="40" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="430" y="195" textAnchor="middle" fontSize="14" fill="currentColor">Reconciliation (every 15 min)</text>

    <rect fill="none" x="270" y="225" width="320" height="40" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="430" y="250" textAnchor="middle" fontSize="14" fill="currentColor">Real-time client</text>

    <rect fill="none" x="270" y="280" width="320" height="40" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="430" y="305" textAnchor="middle" fontSize="14" fill="currentColor">Token service</text>

    <rect fill="none" x="270" y="345" width="320" height="44" rx="8" stroke="currentColor" strokeOpacity="0.5" strokeDasharray="5 4" />

    <text x="430" y="372" textAnchor="middle" fontSize="14" fill="currentColor">Database (MongoDB Atlas)</text>

    <line x1="592" y1="135" x2="718" y2="135" stroke="currentColor" strokeWidth="1.5" markerStart="url(#fx-arrow)" markerEnd="url(#fx-arrow)" />

    <line x1="592" y1="190" x2="718" y2="190" stroke="currentColor" strokeWidth="1.5" markerStart="url(#fx-arrow)" markerEnd="url(#fx-arrow)" />

    <line x1="718" y1="245" x2="592" y2="245" stroke="currentColor" strokeWidth="1.5" markerEnd="url(#fx-arrow)" />

    <line x1="592" y1="300" x2="718" y2="300" stroke="currentColor" strokeWidth="1.5" markerStart="url(#fx-arrow)" markerEnd="url(#fx-arrow)" />

    <rect fill="none" x="700" y="55" width="240" height="360" rx="14" stroke="currentColor" strokeWidth="1.5" />

    <text x="820" y="82" textAnchor="middle" fontSize="16" fontWeight="600" fill="currentColor">Fortnox</text>

    <rect fill="none" x="720" y="115" width="200" height="95" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="820" y="155" textAnchor="middle" fontSize="14" fill="currentColor">REST API v3</text>
    <text x="820" y="177" textAnchor="middle" fontSize="14" fill="currentColor">Warehouse API</text>

    <rect fill="none" x="720" y="225" width="200" height="40" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="820" y="250" textAnchor="middle" fontSize="14" fill="currentColor">Event service (WebSocket)</text>

    <rect fill="none" x="720" y="280" width="200" height="40" rx="8" stroke="currentColor" strokeOpacity="0.5" />

    <text x="820" y="305" textAnchor="middle" fontSize="14" fill="currentColor">OAuth</text>
  </svg>

  <p className="mt-2 text-center text-sm opacity-70">All Fortnox traffic goes through Leara's backend in AWS Stockholm.</p>
</div>

A double arrow means data is both read and written; the single arrow from the Fortnox event service goes only into Leara. Every Fortnox object Leara works with is mirrored in Leara's database, so the apps read from Leara even when Fortnox is slow or unavailable.

## Connection and permissions

Customers connect Fortnox in the Leara web app using OAuth 2.0 (authorization code, offline access). An administrator signs in to Fortnox, approves the permissions, and is sent back to Leara. One Leara account is connected to exactly one Fortnox company; sub-users in Leara use the account's connection and cannot connect their own.

| Scope | Used for |
| - | - |
| `article` | Articles and stock articles |
| `price` | Price lists and prices |
| `customer` | Customer cards |
| `supplier` | Suppliers |
| `order` | Customer orders |
| `offer` | Quotes |
| `invoice` | Invoices and invoice bookkeeping |
| `payment` | Incoming payments on invoices |
| `warehouse` | Warehouses, stock balances, goods receipts, purchase orders, stock transfers, and stocktaking |

Leara does not request `bookkeeping`, `companyinformation`, or `settings`, and therefore has no access to vouchers, the general ledger, or company settings.

### Token handling

* Access tokens and refresh tokens are stored per customer account in Leara's database.
* A separate token service renews the access token every 5 minutes, well before it expires. The main service has a fallback path in case renewal is late.
* Fortnox rotates the refresh token on every use. Writes are conditional (optimistic locking), so two simultaneous renewals cannot break the connection.
* The customer can revoke access in Fortnox at any time. Sync then stops until the connection is set up again.

## Data flows

Every data object has a defined owner and a defined direction. Changes in Leara are written to Fortnox through a queue, normally within seconds; changes in Fortnox reach Leara within seconds via the event stream, or at the latest at the next reconciliation.

| Object | Direction | Trigger | Owner |
| - | - | - | - |
| Articles and product data | Both ways | Real time from Fortnox, reconciliation every 15 min; write queue to Fortnox on every change in Leara | Leara (product data, variants, groups) |
| Prices and price lists | Leara → Fortnox, read back | Change in Leara or price file import, via the write queue | Leara |
| Customers | Both ways | Real time from Fortnox, check every 5 min, full import every 24 h; write when a customer is created in Leara | Leara (customer register); Fortnox owns accounts receivable and credit block status |
| Customer orders | Both ways | Real time from Fortnox, reconciliation every 15 min; write when an order is created or changed in Leara, including e-commerce orders | Leara collects orders from all channels; the order is created in Fortnox |
| Invoices | Both ways | Order → invoice → bookkeeping is created from Leara; real time and reconciliation back | Fortnox (invoice number, bookkeeping, payment status) |
| Incoming payments | Leara → Fortnox | When a payment is registered in Leara | Fortnox |
| Quotes | Both ways | Write when a quote is created in Leara; read on manual sync | Leara |
| Stock balances per warehouse | Fortnox → Leara | After every stock-affecting event, sweep every 60 s, full sweep every night at 03:00 | Fortnox, when the Fortnox inventory module is used |
| Warehouses | Fortnox → Leara | Read and cached for 10 min | Fortnox |
| Goods receipts | Leara → Fortnox | Created, updated, and completed from Leara | Leara |
| Purchase orders | Both ways | Write from Leara; reconciliation every 30 min (Fortnox has no events for purchase orders) | Leara |
| Stock transfers | Leara → Fortnox | When a transfer between warehouses is completed in Leara | Leara |
| Stocktaking and stock adjustments | Leara → Fortnox | On completed stocktake or manual adjustment; booked first in Fortnox, then in Leara | Leara (count), Fortnox (booked balance) |
| Suppliers | Both ways | Read page by page; created from Leara | Fortnox |
| Delivery methods | Fortnox → Leara | Read and cached | Fortnox |

<Note>
  If the customer does not use the Fortnox inventory module, Leara can own the stock balance instead. This is chosen per account.
</Note>

## Real-time sync and reconciliation

Fortnox data reaches Leara in three layers, so a lost message never becomes a permanent difference.

<Steps>
  <Step title="Real time">
    Leara keeps a WebSocket connection to the Fortnox event service (`wss://ws.fortnox.se/topics-v1`) and subscribes to the topics articles, orders, invoices, and customers. An event only contains which object changed; Leara then fetches the object through the REST API and updates its copy. The position in the stream is saved per account and topic, so on reconnect Leara continues where it left off (Fortnox keeps events for about 14 days).
  </Step>

  <Step title="Reconciliation every 15 minutes">
    A scheduled job asks Fortnox for everything that changed since the previous run, with a 10-minute overlap, for orders, invoices, and articles. Everything found is fetched and rewritten. It does not matter if an object is already up to date, since the update is idempotent. The starting point is saved in the database and survives restarts.
  </Step>

  <Step title="Full sweeps">
    Customers are fully re-imported every 24 hours, stock balances every night, and purchase orders every 30 minutes. A full sync of articles, orders, invoices, and quotes runs on first connection and can be started manually.
  </Step>
</Steps>

The connection is monitored. If it drops, Leara reconnects automatically with increasing wait time (1 to 60 s) and re-registers the accounts with fresh tokens. Deleted objects are caught via the real-time event or when Leara gets a 404 on fetch, since Fortnox change queries do not list deleted records. When Leara itself writes to Fortnox, the change comes back as an event. For articles, Leara recognizes its own writes and does not let an echo overwrite a newer local change.

## Writing to Fortnox

Leara stays well below the Fortnox rate limit and retries failed writes automatically. Fortnox allows 25 requests per 5 seconds per company; Leara's shared request budget is 3.5 requests per second with a peak of 8. Flows outside the budget throttle themselves and handle 429 responses.

* **Durable write queue for articles and prices.** Every change is placed in a queue in the database before it is sent, so a restart loses nothing. Multiple changes to the same article are merged into one write with the article's current values. Changes from users take priority over bulk imports.
* **Budget split in two.** Half the budget is reserved for ongoing sync and half for heavy exports (for example stocktaking), so a large import does not stop day-to-day flow.
* **Retries.** On a 429 response (too many requests), Leara waits and tries again without counting it as an error. On temporary errors, up to 5 attempts are made with increasing wait time (up to 10 minutes). After that, the record is marked as failed and shown to the user with an option to retry.
* **Idempotency.** E-commerce orders carry a unique reference in Fortnox, so an order is never created twice on retry. Stock transfers with an unknown outcome are reconciled afterwards instead of being resent.
* **Block for unsynced articles.** An order, quote, or invoice cannot be sent to Fortnox with an article that does not yet exist there.
* **Visible status.** Every article shows whether it is synced, pending, or failed. Failed orders and import rows are shown with an explanation.

## Dependencies

The integration depends on Fortnox's public APIs, AWS, and MongoDB Atlas. No other third-party services are in the data flow between Leara and Fortnox.

| Dependency | Used for |
| - | - |
| Fortnox OAuth (`apps.fortnox.se/oauth-v1`) | Connection and token renewal |
| Fortnox REST API v3 (`api.fortnox.se/3`) | Articles, prices, price lists, customers, suppliers, orders, quotes, invoices, incoming payments, delivery methods |
| Fortnox Warehouse API (`api.fortnox.se/api/warehouse`) | Warehouses, stock balances, goods receipts, purchase orders, stock transfers, stocktaking |
| Fortnox WebSocket (`ws.fortnox.se/topics-v1`) | Real-time events |
| AWS ECS, eu-north-1 | Runs Leara's backend and token service |
| MongoDB Atlas | Leara's data model, mirrored Fortnox objects, write queue, sync status, and tokens |

The backend is written in Java 17 with Quarkus. Fortnox is called with the JDK HTTP client, without a Fortnox SDK. If Fortnox is unavailable, Leara keeps working: article and price changes stay in the queue, other writes are shown as failed and can be retried, and the sync catches up on what was missed when Fortnox responds again.

## Security, multi-tenancy, and data protection

Each customer's data and Fortnox connection is kept isolated per Leara account, and all traffic is encrypted.

* **Isolation.** All mirrored Fortnox objects are stored with the account ID as part of the key, and tokens, sync status, and queues are kept per account.
* **One connection per account.** The token for a Fortnox company is only used for that account's requests. Real-time events arrive tagged with the Fortnox company identity and are routed to the right account.
* **Least privilege.** Leara only requests the scopes the integration uses (see above) and has no access to bookkeeping vouchers or company settings.
* **Transport.** All requests to Fortnox use HTTPS and WSS respectively. Leara's API is reached over HTTPS at `api.leara.se`.
* **Data location.** The backend and token service run in AWS eu-north-1 (Stockholm).
* **Personal data.** The integration handles customers' names, addresses, email addresses, and phone numbers from customer cards, orders, and invoices. This data is only used to keep Leara and Fortnox in sync.
* **Revocation.** The customer can revoke Leara's access in Fortnox at any time.

## Operations and monitoring

Sync health is tracked per customer account in Leara's internal admin tool, and missed changes are caught up automatically by the reconciliation.

* A health check reports whether the real-time connection is up, when the reconciliation last succeeded, and whether the request budget has run out.
* The admin tool shows token status per account (valid, expiring soon, stale, failing, missing) and creates notifications every 15 minutes for accounts that need attention.
* Logs are collected in AWS CloudWatch.

## Known limitations

Most limitations come from Fortnox's own APIs. Here they are, and how Leara handles them.

* Changes in Fortnox are normally visible within seconds, but in the worst case after up to about 15 minutes, when the reconciliation catches a missed event.
* Large bulk imports take time because Fortnox limits the number of requests. An article with several price lists costs one request per price list.
* Refresh tokens expire after 45 days without use, and the customer can revoke them in Fortnox. The connection must then be set up again by an administrator.
* A Fortnox company can only be connected to one Leara environment at a time, since Fortnox rotates the token on every renewal. A test environment needs its own Fortnox company, for example a test company.
* Fortnox has no real-time events for quotes and purchase orders. Purchase orders are reconciled every 30 minutes and quotes are read on manual sync.
* The Fortnox stock balance API has no bin location level. Leara shows stock per warehouse from Fortnox.
* Fortnox change queries do not list deleted records. Deletions are caught via real-time events.

For setup steps, see [Connect your ERP](/integrations/erp).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.