Skip to content
FortnoxFortnox API — code examples

Fortnox API code examples

Four working examples against the Fortnox API, each in both Python and TypeScript: fetch unpaid invoices, create an invoice, page through the supplier register, and handle 429 with backoff.

All four examples authenticate the same way: a Bearer token in the Authorization header against https://api.fortnox.se/3. Swap ACCESS_TOKEN (Python) or accessToken (TypeScript) for a real token before you run the code.

You don't type that token in by hand — it comes from Fortnox's OAuth 2.0 flow and renews with a refresh token. How authentication works step by step.

Fetch unpaid customer invoices

GET /3/invoices with the filter=unpaid query parameter returns only unpaid invoices — the Fortnox OpenAPI spec defines the filter alongside cancelled, fullypaid, unpaidoverdue, and unbooked. The response is an Invoices list where every entry carries a Total field, so the example counts and sums it directly.

Python

import requests

ACCESS_TOKEN = "..."  # from the Fortnox OAuth flow, see /fortnox-api/auth

response = requests.get(
    "https://api.fortnox.se/3/invoices",
    headers={"Authorization": f"Bearer {ACCESS_TOKEN}"},
    params={"filter": "unpaid"},
)
response.raise_for_status()

invoices = response.json()["Invoices"]
total = sum(invoice["Total"] for invoice in invoices)
print(f"{len(invoices)} obetalda fakturor, totalt {total}")

TypeScript

const accessToken = "..."; // from the Fortnox OAuth flow, see /fortnox-api/auth

const response = await fetch(
  "https://api.fortnox.se/3/invoices?filter=unpaid",
  { headers: { Authorization: `Bearer ${accessToken}` } },
);
if (!response.ok) {
  throw new Error(`Fortnox request failed: ${response.status}`);
}

const { Invoices } = await response.json();
const total = Invoices.reduce((sum, invoice) => sum + invoice.Total, 0);
console.log(`${Invoices.length} unpaid invoices, totaling ${total}`);

Example · sample invoices

ClaudeClaudeUnpaid invoices
Show unpaid invoices
MCP tool call · Fortnox customer invoices
Unpaid
Customer invoices · unpaid
No.DateCustomerTotal
10013 JunKvarnen AB12 500
10049 JunNordic Bygg6 250
100717 JunLindqvist & Co3 750
3 invoices · total22 500 kr
Ask about your bookkeeping…
Connected servers: Nordsynk
Claude
A read: the answer comes straight from Fortnox, with no client file and no approval.

Create a customer invoice

POST /3/invoices takes an Invoice payload. Per the bundled OpenAPI spec, CustomerNumber is the only required field on the Invoice schema — the example also adds one InvoiceRows entry so the invoice is actually sendable. A successful call returns status 201 and the created invoice.

Python

import requests

ACCESS_TOKEN = "..."  # from the Fortnox OAuth flow, see /fortnox-api/auth

payload = {
    "Invoice": {
        "CustomerNumber": "1",  # the only required field on the Invoice schema
        "InvoiceRows": [
            {"Description": "Konsulttimmar", "Price": 1200, "VAT": 25},
        ],
    }
}

response = requests.post(
    "https://api.fortnox.se/3/invoices",
    headers={"Authorization": f"Bearer {ACCESS_TOKEN}"},
    json=payload,
)
if response.status_code != 201:
    raise RuntimeError(f"Fortnox request failed: {response.status_code} {response.text}")

invoice = response.json()["Invoice"]
print(f"Created invoice {invoice['DocumentNumber']} for {invoice['Total']}")

TypeScript

const accessToken = "..."; // from the Fortnox OAuth flow, see /fortnox-api/auth

const response = await fetch("https://api.fortnox.se/3/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    Invoice: {
      CustomerNumber: "1", // the only required field on the Invoice schema
      InvoiceRows: [{ Description: "Konsulttimmar", Price: 1200, VAT: 25 }],
    },
  }),
});
if (!response.ok) {
  throw new Error(`Fortnox request failed: ${response.status}`);
}

const { Invoice } = await response.json();
console.log(`Created invoice ${Invoice.DocumentNumber} for ${Invoice.Total}`);

Example · sample invoice

Preview · Fortnox

Create customer invoice for customer 1, 1 500 kr

Awaiting approval

Requested via Claude · Acme AB

Customer numberIn Fortnox now—After approval1
Invoice row · KonsulttimmarIn Fortnox now—After approval1,200.00
VAT 25 %In Fortnox now—After approval300.00
TotalIn Fortnox now—After approval1,500.00
ApproveReject

Nothing is written to Fortnox until you approve, unless the company has enabled auto-approve.

A write: you see exactly what will be created and approve or reject it. The preview can't be edited.

Handle pagination

GET /3/suppliers returns a Suppliers list with no page metadata in the bundled spec. Instead of reading a TotalPages field that isn't in the schema, the example loops on page and limit until a page comes back shorter than limit.

Python

import requests

ACCESS_TOKEN = "..."  # from the Fortnox OAuth flow, see /fortnox-api/auth
PAGE_SIZE = 100

suppliers = []
page = 1
while True:
    response = requests.get(
        "https://api.fortnox.se/3/suppliers",
        headers={"Authorization": f"Bearer {ACCESS_TOKEN}"},
        params={"page": page, "limit": PAGE_SIZE},
    )
    response.raise_for_status()

    batch = response.json()["Suppliers"]
    suppliers.extend(batch)
    if len(batch) < PAGE_SIZE:
        break
    page += 1

print(f"{len(suppliers)} leverantörer")

TypeScript

const accessToken = "..."; // from the Fortnox OAuth flow, see /fortnox-api/auth
const PAGE_SIZE = 100;

const suppliers: unknown[] = [];
let page = 1;

while (true) {
  const response = await fetch(
    `https://api.fortnox.se/3/suppliers?page=${page}&limit=${PAGE_SIZE}`,
    { headers: { Authorization: `Bearer ${accessToken}` } },
  );
  if (!response.ok) {
    throw new Error(`Fortnox request failed: ${response.status}`);
  }

  const { Suppliers } = await response.json();
  suppliers.push(...Suppliers);
  if (Suppliers.length < PAGE_SIZE) break;
  page += 1;
}

console.log(`${suppliers.length} suppliers`);

Handle 429 with backoff

HTTP 429 means you've hit the quota (25 calls per 5 seconds). The wrapper below uses the same defaults as our production client: three retries, exponential backoff from 1 to 30 seconds with jitter, and it honors the Retry-After header when Fortnox sends one.

Python

import random
import time
import requests

MAX_RETRIES = 3
BASE_DELAY_S = 1
MAX_DELAY_S = 30

def fortnox_get(path, access_token, **params):
    for attempt in range(MAX_RETRIES + 1):
        response = requests.get(
            f"https://api.fortnox.se/3{path}",
            headers={"Authorization": f"Bearer {access_token}"},
            params=params,
        )
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()

        if attempt == MAX_RETRIES:
            response.raise_for_status()

        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else min(BASE_DELAY_S * 2 ** attempt, MAX_DELAY_S)
        time.sleep(delay + random.uniform(0, delay * 0.5))  # jitter

ACCESS_TOKEN = "..."  # from the Fortnox OAuth flow, see /fortnox-api/auth
data = fortnox_get("/invoices", ACCESS_TOKEN, filter="unpaid")

TypeScript

const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1_000;
const MAX_DELAY_MS = 30_000;

async function fortnoxGet(path: string, accessToken: string): Promise<unknown> {
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
    const response = await fetch(`https://api.fortnox.se/3${path}`, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    if (response.status !== 429) {
      if (!response.ok) {
        throw new Error(`Fortnox request failed: ${response.status}`);
      }
      return response.json();
    }

    if (attempt === MAX_RETRIES) {
      throw new Error("Fortnox rate limit: retries exhausted");
    }

    const retryAfter = response.headers.get("Retry-After");
    const backoff = Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
    const delayMs = retryAfter ? Number(retryAfter) * 1000 : backoff;
    await new Promise((resolve) => setTimeout(resolve, delayMs + Math.random() * delayMs * 0.5));
  }

  throw new Error("Fortnox rate limit: retries exhausted");
}

const accessToken = "..."; // from the Fortnox OAuth flow, see /fortnox-api/auth
const data = await fortnoxGet("/invoices?filter=unpaid", accessToken);

Or skip the code

Nordsynk connects an AI assistant straight to Fortnox over MCP. The same four tasks become one line of text instead of a client file to maintain:

The same four tasks · two ways

With your own script
  1. 01Fetch unpaid invoices
  2. 02Create an invoice
  3. 03Page through suppliers
  4. 04Handle 429 with backoff
With Nordsynk
› Show unpaid invoices

Nordsynk connects an AI assistant straight to Fortnox over MCP. The four tasks become one line of text instead of a client file to maintain.

Retries and 429s are already handled, so what you write is one sentence.
  • “show unpaid invoices”
  • “create an invoice”
  • “list suppliers”
  • “no need to handle 429 — we take care of retries”