API

Guide 3 af 8

Paginering

Alle lister i v1 er cursor-pagineret. Der er ingen page-parameter, ingen total, og ingen "gå til side 7".

Formen

{
  "data": [ /* rækkerne */ ],
  "meta": { "per_page": 50 },
  "links": { "next": "https://app.levano.io/api/v1/matters?per_page=50&cursor=eyJpZCI6..." }
}

To parametre styrer det:

Parameter Betydning
per_page Sidestørrelse. Standard 50, maksimum 100
cursor Uigennemsigtig markør. Du opfinder den ikke, du følger den

Beder du om mere end 100, får du 100. Beder du om nul eller et negativt tal, får du standarden. Ingen af delene er en fejl.

Løkken

Følg links.next indtil den er null. Det er hele algoritmen.

url="https://app.levano.io/api/v1/time-entries?per_page=100"

while [ -n "$url" ] && [ "$url" != "null" ]; do
  body=$(curl -sS "$url" -H "Authorization: Bearer $LEVANO_KEY" -H "Accept: application/json")
  echo "$body" | jq -c '.data[]'
  url=$(echo "$body" | jq -r '.links.next')
done
import requests

url = "https://app.levano.io/api/v1/time-entries?per_page=100"
headers = {"Authorization": f"Bearer {key}", "Accept": "application/json"}

while url:
    body = requests.get(url, headers=headers, timeout=30).json()
    for row in body["data"]:
        handle(row)
    url = body["links"]["next"]

links.next er en absolut URL med alle dine oprindelige query-parametre bevaret. Du skal ikke sætte den sammen selv, og du skal ikke bære dine filtre med over manuelt.

Behandl cursoren som ugennemsigtig

Cursoren er base64 over et internt sorteringsnøglepar. Format og indhold er ikke en del af kontrakten og kan ændre sig uden en versionsbump. Gem den ikke i en database som et "sted i listen" du vender tilbage til i næste uge, og byg den ikke selv.

Vil du hente ændringer siden sidst, så filtrér på et datofelt frem for at gemme en cursor. Se næste afsnit.

Hvorfor ikke offset

Offset-paginering (?page=4) har to problemer der begge rammer i praksis:

  1. Rækker flytter sig. Bliver der oprettet en sag mens du henter side 3, skubbes en række fra side 3 ned på side 4, og du får den to gange. Bliver en slettet, springer du en over.
  2. Dyb paginering bliver dyr. OFFSET 50000 betyder at databasen tæller sig frem gennem 50.000 rækker for at kaste dem væk.

Cursor-paginering læser videre fra det sidste sete sorteringsnøglepar, så den er stabil under samtidige skrivninger og lige hurtig på side 1 og side 900.

Filtrér frem for at hente alt

Listerne tager filtre, og de kombineres med paginering: filtrene bæres automatisk med over i links.next.

Liste Filtre
/matters client_id, status, updated_since
/clients type, updated_since
/people updated_since
/companies updated_since, cvr
/time-entries matter_id, client_id, user_id, date_from, date_to, status
/invoices client_id, matter_id, status, issue_date_from, issue_date_to
/documents matter_id

updated_since er den rigtige måde at lave en trinvis synkronisering på:

curl -sS "https://app.levano.io/api/v1/matters?updated_since=2026-03-01T00:00:00Z&per_page=100" \
  -H "Authorization: Bearer $LEVANO_KEY" \
  -H "Accept: application/json"

updated_since er ISO-8601 og tager alt der er ændret på eller efter tidspunktet. Sender du et offset frem for Z, skal + URL-kodes som %2B, ellers læses det som et mellemrum.

Gem tidsstemplet for hvornår du kørte, ikke cursoren. Næste kørsel spørger efter det tidspunkt. Overlap et minut eller to for at være sikker, og lad deduplikeringen på id tage resten.

Den fulde parameterliste pr. endpoint står i referencen.

Sortering

Lister er sorteret nyeste først, på oprettelsestidspunkt med id som tiebreaker. Rækkefølgen er en del af kontrakten fordi cursoren afhænger af den. Der er ingen sort-parameter i v1.

Ingen totaltælling

meta indeholder per_page, og ikke andet. Der er bevidst ingen total: et præcist tal kræver en COUNT over hele tabellen ved hver eneste side, hvilket er den dyreste del af forespørgslen og næsten aldrig den brugeren læser.

Har du brug for tal frem for rækker, så brug rapport-endpointet i stedet for at tælle en liste igennem.

KYC-API'en pagineres anderledes

/api/kyc/v1 bruger klassisk sidepaginering: page og per_page ind, og et meta-objekt med current_page, per_page, total og last_page ud. Der er ingen links.next at følge, og der er et totaltal.

Det er ikke en fejl, og de to skemaer bliver ikke ensrettet uden en versionsbump på den ene af dem. Læs meta for den kontrakt du faktisk kalder, frem for at antage at den ligner den anden.