Jämför Bokföring
Teknik

Fortnox API: komplett guide för utvecklare 2026

Fortnox API-guide: OAuth2-flow, endpoints, Python-exempel, rate limits och vanliga fel. Skapa faktura, hämta kunder och bygg integrationer.

Av Jämför Bokföring-redaktionen Uppdaterad 2026-07-28

Fortnox API är det mest utbyggda REST-API:et bland de svenska bokföringsprogrammen: öppen dokumentation, OAuth2, bredast endpoint-täckning och det största partnerekosystemet. Det finns ett undantag värt att känna till — Spiris är det enda av programmen som dessutom har en sandbox-miljö. Fortnox har ingen, så all utveckling sker mot en riktig eller demo-tenant.

Den här guiden täcker det du behöver för att komma igång: OAuth2-flödet, hur du skapar en faktura, hanterar rate limits och undviker de vanligaste fallgroparna. Kodexemplen bygger på Fortnox publika API-dokumentation — verifiera alltid mot den aktuella versionen på apps.fortnox.se/apidocs innan du bygger vidare, eftersom endpoints och fältnamn ändras över tid.

Snabbstart på 10 minuter

Innan du ens öppnar en editor behöver du tre saker:

  1. En Fortnox-licens (din egen demo-tenant räcker — 6 mån gratis via kod NYSTARTAD).
  2. Ett utvecklarkonto på developer.fortnox.se — här registrerar du din integration och får client_id + client_secret.
  3. En kund (eller test-tenant) som godkänner din app — utan det får du inte ut någon access_token.

Integrationen är en OAuth2-app. Fortnox är ovanligt strikta med att man registrerar exakta scopes i förväg (t.ex. invoice, customer, bookkeeping). Begär bara det du faktiskt använder — det är svårare att lägga till scopes senare utan att användaren godkänner om.

OAuth2-flow: så funkar det i Fortnox

Fortnox använder en lätt modifierad variant av OAuth2 Authorization Code-flödet. Stegen är:

  1. Din app skickar användaren till Fortnox authorization-URL med client_id, scope och state.
  2. Användaren loggar in och godkänner scopes.
  3. Fortnox redirectar tillbaka till din redirect_uri med en code-parameter.
  4. Din backend byter code mot access_token + refresh_token via POST https://apps.fortnox.se/oauth-v1/token.
  5. access_token är giltig i 1 timme. refresh_token används för att förnya.
import requests
import base64

CLIENT_ID = "din-client-id"
CLIENT_SECRET = "din-client-secret"
REDIRECT_URI = "https://dinapp.se/callback"

def exchange_code_for_token(code: str) -> dict:
    auth = base64.b64encode(
        f"{CLIENT_ID}:{CLIENT_SECRET}".encode()
    ).decode()
    r = requests.post(
        "https://apps.fortnox.se/oauth-v1/token",
        headers={
            "Authorization": f"Basic {auth}",
            "Content-Type": "application/x-www-form-urlencoded",
        },
        data={
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": REDIRECT_URI,
        },
        timeout=15,
    )
    r.raise_for_status()
    return r.json()

Svaret ser ut ungefär så här:

{
  "access_token": "eyJ0eXAiOiJKV1Qi...",
  "refresh_token": "a1b2c3d4...",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": "invoice customer bookkeeping"
}

Viktig detalj: spara refresh_token direkt, innan du gör något annat med svaret. Tappar du bort den finns ingen väg tillbaka utan att köra hela auktorisationsflödet igen — vilket kräver att slutanvändaren loggar in och godkänner på nytt. Refresh_token går att återanvända tills användaren återkallar tillståndet.

Autentisering: access_token och refresh

Alla API-anrop skickas med en Authorization: Bearer {access_token}-header mot https://api.fortnox.se/3/. När token löpt ut får du 401 — då förnyar du så här:

def refresh_access_token(refresh_token: str) -> dict:
    auth = base64.b64encode(
        f"{CLIENT_ID}:{CLIENT_SECRET}".encode()
    ).decode()
    r = requests.post(
        "https://apps.fortnox.se/oauth-v1/token",
        headers={
            "Authorization": f"Basic {auth}",
            "Content-Type": "application/x-www-form-urlencoded",
        },
        data={
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
        },
        timeout=15,
    )
    r.raise_for_status()
    return r.json()

En vanlig fallgrop: Fortnox kan rotera refresh_token vid varje förnyelse. Glömmer du skriva över den sparade kopian slutar nästa refresh att fungera, och du måste köra hela auktorisationsflödet igen med slutanvändaren. Skriv därför alltid över båda tokens i din databas i samma transaktion.

Endpoints översikt

Fortnox /3/-API:et är brett. Här är de endpoints du kommer att använda oftast:

EndpointBeskrivningMetoder
/3/invoicesKundfakturorGET, POST, PUT
/3/customersKunderGET, POST, PUT, DELETE
/3/suppliersLeverantörerGET, POST, PUT
/3/vouchersVerifikatGET, POST
/3/accountsKontoplanGET, PUT
/3/articlesArtiklar/produkterGET, POST, PUT, DELETE
/3/ordersOrderläggningGET, POST, PUT
/3/offersOfferterGET, POST
/3/financialyearsRäkenskapsårGET
/3/employeesAnställdaGET, POST, PUT

Fullständig referens finns på apps.fortnox.se/apidocs. API:et returnerar både JSON och XML — ange Accept: application/json i alla anrop om du kör Python eller JavaScript.

Skapa en faktura (Python + cURL)

Det här är hello-world för Fortnox-integrationer. Du behöver först en kund (CustomerNumber). Här antar vi att kunden redan finns.

Python

import requests

ACCESS_TOKEN = "din-access-token"

def create_invoice(customer_number: str, rows: list) -> dict:
    payload = {
        "Invoice": {
            "CustomerNumber": customer_number,
            "InvoiceDate": "2026-04-15",
            "DueDate": "2026-05-15",
            "InvoiceRows": rows,
        }
    }
    r = requests.post(
        "https://api.fortnox.se/3/invoices",
        headers={
            "Authorization": f"Bearer {ACCESS_TOKEN}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
        json=payload,
        timeout=15,
    )
    if r.status_code == 429:
        raise RuntimeError("Rate limit — vänta 5 sek och försök igen")
    r.raise_for_status()
    return r.json()

rows = [
    {
        "ArticleNumber": "KONSULT",
        "Description": "Konsulttimmar april",
        "DeliveredQuantity": 10,
        "Price": 1200,
        "VAT": 25,
    }
]

invoice = create_invoice("1", rows)
print(invoice["Invoice"]["DocumentNumber"])

cURL

curl -X POST https://api.fortnox.se/3/invoices \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "Invoice": {
      "CustomerNumber": "1",
      "InvoiceDate": "2026-04-15",
      "DueDate": "2026-05-15",
      "InvoiceRows": [
        {"ArticleNumber": "KONSULT", "DeliveredQuantity": 10, "Price": 1200, "VAT": 25}
      ]
    }
  }'

Fortnox returnerar hela fakturan med DocumentNumber, Total, Balance m.fl. Spara DocumentNumber — det är den primära nyckeln du använder för att uppdatera, skicka eller makulera fakturan senare.

Hämta kunder

def list_customers(page: int = 1, limit: int = 100) -> dict:
    r = requests.get(
        f"https://api.fortnox.se/3/customers",
        headers={
            "Authorization": f"Bearer {ACCESS_TOKEN}",
            "Accept": "application/json",
        },
        params={"page": page, "limit": limit},
        timeout=15,
    )
    r.raise_for_status()
    return r.json()

data = list_customers()
for c in data["Customers"]:
    print(c["CustomerNumber"], c["Name"])

Paginering är sparsamt dokumenterad hos Fortnox. MetaInformation i svaret innehåller @TotalResources, @TotalPages och @CurrentPage — använd dem för att iterera. Max limit är 500. En lägre siffra, runt 100, ger mindre svar och gör felsökningen enklare utan att kosta särskilt många extra anrop.

cURL-variant för enkel koll

curl https://api.fortnox.se/3/customers?limit=5 \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json"

Webhooks

Fortnox Webhooks ligger i beta enligt dokumentationen 2026 och stöder i dagsläget ett begränsat antal händelser — främst invoice.created, invoice.paid och voucher.created. Du registrerar webhooks via /3/webhooks-endpointen.

def register_webhook(url: str, event: str) -> dict:
    payload = {
        "Webhook": {
            "Url": url,
            "Event": event,
        }
    }
    r = requests.post(
        "https://api.fortnox.se/3/webhooks",
        headers={
            "Authorization": f"Bearer {ACCESS_TOKEN}",
            "Content-Type": "application/json",
        },
        json=payload,
        timeout=15,
    )
    r.raise_for_status()
    return r.json()

Webhooks signeras med en HMAC-header — verifiera alltid signaturen innan du litar på payloaden. Sekreten får du när webhooken registreras.

Nackdel som är värd att nämna: Fortnox webhooks täcker färre händelsetyper än vad man är van vid från renodlade betaltjänster. För kritiska flöden — bokföring av inbetalningar, till exempel — är det klokt att komplettera med ett pollingjobb som failsafe i stället för att lita på att varje webhook kommer fram.

Rate limits och felhantering

Fortnox rate limit enligt dokumentationen 2026 är 25 requests per 5 sekunder per access_token — alltså ungefär 300 req/min. Det låter generöst men är lätt att träffa om du bulk-importerar kunder eller fakturor.

Felkoderna du kommer se:

StatusBetydelseÅtgärd
401Access_token utgången eller ogiltigKör refresh_token-flow
403Saknar scopeRe-auktorisera med rätt scope
404Resurs finns inteKolla DocumentNumber/CustomerNumber
429Rate limitBacka av, vänta 5 sek
400ValideringsfelLäs ErrorInformation.message i svaret
500Fortnox-felRetry med exponential backoff

Enkel retry-wrapper:

import time

def with_retry(fn, max_retries: int = 5):
    for attempt in range(max_retries):
        try:
            return fn()
        except requests.HTTPError as e:
            status = e.response.status_code
            if status == 429:
                time.sleep(5 * (attempt + 1))
            elif status >= 500:
                time.sleep(2 ** attempt)
            else:
                raise
    raise RuntimeError("Max retries nådda")

Ett vanligt 400-fel beror på att CustomerNumber skickas som heltal i stället för sträng. Fortnox är strikt med typer — alla ID-fält ska vara strängar, även när värdet ser ut som ett tal.

Begränsningar — var ärlig

Fortnox API är bra, men inte perfekt:

  • Ingen separat sandbox. Du testar mot en riktig (men gratis demo-) tenant. Jämför med Visma eEkonomi som har dedikerad sandbox.
  • Webhooks är fortfarande begränsade. För realtid i produktion kompletterar du med polling.
  • Paginering är inkonsekvent mellan olika endpoints — vissa använder @TotalPages, andra returnerar bara data.
  • Svarsformat har legacy-XML-arv — fältnamn är PascalCase och nästlade i konstiga objektnivåer (Invoice.InvoiceRows.InvoiceRow).

För en utvecklarvinkel på hur Fortnox står sig mot andra bokföringsprogram, se vår jämförelse av bokföringsprograms API:er.

FAQ

Är Fortnox API gratis?

API-åtkomsten i sig är gratis — men du behöver en Fortnox-licens (från 209 kr/mån, se Fortnox-prisanalys) för att ha en tenant att integrera mot.

Finns det en sandbox?

Nej. Fortnox har ingen separat sandbox, utan du kör mot en riktig tenant (ofta demo-tenant med 6 mån gratis). Det är en svaghet jämfört med Visma eEkonomi.

Hur länge gäller en access_token?

1 timme (3600 sekunder). Refresh_token är långlivad och används för att förnya tills användaren återkallar tillstånd.

Vilka språk har officiella SDK:er?

Fortnox publicerar inget officiellt SDK. Det finns community-paket för Python (fortnox-python), PHP och Node, men de underhålls i olika takt och ligger inte alltid i fas med API:ets senaste ändringar. Att gå direkt mot REST-API:et ger färre överraskningar och gör felsökningen enklare — det är också vad exemplen i den här guiden gör.

Kan jag automatisera hela bokföringen via API?

I princip ja — verifikat (/3/vouchers), fakturor och kontoplan är alla tillgängliga. Men slutlig deklaration och årsredovisning sker fortfarande i Fortnox-UI. Se vår guide automatisera fakturering för ett komplett exempel.

Vad kostar det att bygga en integration?

Utvecklartid. Fortnox tar ingen API-avgift för standardintegrationer. Om du paketerar din integration i deras Fortnox Appstore får Fortnox provision.


Fortnox är det rimliga förstavalet om du bygger en svensk bokföringsintegration och vill nå flest möjliga kunder — ekosystemet och antalet befintliga integrationer är störst. Dokumentationen har luckor och webhooks är begränsade, och saknaden av sandbox betyder att du får vara försiktig med vilken tenant du utvecklar mot. Behöver du en isolerad testmiljö är Spiris API värt att titta på i stället. Börja med OAuth2-flödet, skapa en faktura, och bygg ut därifrån.

Läs vidare