Skip to main content

API og autentisering

API-et serveres fra https://api.utlegg.app. Den interaktive referansen ligger på api.utlegg.app/api-docs (Swagger UI), og rå OpenAPI-spec på api.utlegg.app/api-docs-json.

Header-format

Alle autentiserte kall sender token i Authorization-headeren:

Authorization: Bearer <token>

Tokens har prefiks som forteller hva de er:

PrefiksTypeBrukes for
ua_Personal Access Token (PAT)Personlige integrasjoner og utviklerbruk
uas_Service Access Token (SAT)Maskin-til-maskin (organisasjon eller globalt)
(JWT)Bruker-sessionWeb/mobil etter ordinær innlogging

API-et identifiserer token-type ut fra prefikset; du trenger ikke spesifisere dette eksplisitt.

Personal Access Tokens (PAT)

En PAT er knyttet til din egen brukerkonto og har samme rettigheter som deg selv i de organisasjonene du tilhører. Bruk den til scripts, ad hoc-integrasjoner og utviklertesting.

Lag en PAT i webdashbordet

  1. Logg inn på dashbordet.
  2. Gå til Profil → Tokens (eller Innstillinger → Personlige tokens).
  3. Klikk Ny token, gi den et navn og eventuelt en utløpsdato.
  4. Kopier tokenen umiddelbart — den vises kun én gang.

Lag en PAT via API-et

curl -X POST https://api.utlegg.app/users/pat \
-H "Authorization: Bearer <din-eksisterende-token>" \
-H "Content-Type: application/json" \
-d '{"name": "min-integrasjon", "expiresAt": "2026-12-31T00:00:00Z"}'

Andre PAT-endepunkter:

  • GET /users/pat — list dine egne PATs
  • DELETE /users/pat/:id — tilbakekall en PAT

Sikkerhetsregler

  • Lagre PAT som en hemmelighet (env var, secret store) — aldri i kildekode eller logger.
  • Sett alltid expiresAt der det er praktisk.
  • Tilbakekall ubrukte tokens.
  • En PAT mister tilgang umiddelbart hvis brukeren deaktiveres eller mister sin organisasjonsrolle.

Service Access Tokens (SAT)

En SAT er en maskin-til-maskin-token knyttet til en service account i en organisasjon (eller globalt for partnere). Den brukes når en integrasjon skal kjøre uten en fysisk innlogget bruker.

SAT må bestilles

SAT-er kan i dag ikke opprettes selvbetjent fra dashbordet. Ta kontakt på chat på utleggsappen.no og oppgi:

  • Organisasjonens handle eller ID
  • Bruksområde (f.eks. "synk mot Tripletex", "egen ekstern godkjenningsapp")
  • Hvilken rolle / hvilke ressurser tokenen trenger tilgang til
  • Forventet kallvolum
  • Kontaktperson som eier integrasjonen
  • Sikker leveringskanal for tokenen (1Password Send, kryptert e-post el.l.)

Vi oppretter da en service account, kobler riktige rettigheter, og leverer tokenen tilbake. SAT-er har prefiks uas_.

SAT-administrasjonsendepunktene under /organizations/:organizationId/service-accounts finnes, men er i dag interne og ikke i den offentlige Swagger-tag-listen.

Organisasjonskontekst

De fleste ressurser er organisasjonsavgrenset, og endepunkter har formen:

/organizations/{organizationId}/...

For å finne dine organisasjons-IDer:

curl https://api.utlegg.app/users/me \
-H "Authorization: Bearer ua_..."

Responsen inkluderer hvilke organisasjoner brukeren tilhører.

For SCIM brukes organisasjonens handle i stedet for ID:

/organizations/{organization_handle}/scim/Users

Du finner handle i URL-en på dashbordet (https://utlegg.app/{organization_handle}). SCIM-endepunktene autentiseres med en PAT fra en admin-bruker — se SCIM-referansen.

Standard responsformater

Paginering

Lister returneres som:

{
"data": [ ... ],
"totalCount": 137,
"count": 50
}

totalCount er det totale antallet rader som matcher filteret (uavhengig av paginering), count er antall i nåværende side, og data er sidens rader.

Lister og filtrering (query, sort, limit, offset)

Alle findAll-endepunkter (lister) tar et felles sett query-parametere som parses med qs. Det betyr at du bruker bracket-syntaks for nøstede objekter — ikke JSON-strenger eller dot-notation.

ParameterTypeBeskrivelse
queryobject (bracket-syntaks)Filtre, mappes direkte til Prisma where
sortobject (bracket-syntaks)Sorteringsnøkler, mappes til Prisma orderBy
limitnumber ≥ 1Max rader pr. side
offsetnumber ≥ 0Offset i resultatet

qs er konfigurert med depth: 7, strictDepth: true — gå dypere enn 7 nivåer og kallet feiler med 400.

Eksempler

Disse eksemplene treffer reelle endepunkter — feltnavnene er Prisma-modellene direkte (se Swagger eller responsobjektet for hvilke felt som finnes).

Hent siste 20 rapporter, nyeste først:

GET /reports?limit=20&sort[createdAt]=desc

Alle rapporter som venter på godkjenning i en organisasjon:

GET /reports?query[organizationId]=01HXY...&query[status]=PENDING

Rapporter med flere statuser (komma blir IN):

GET /reports?query[status]=PENDING,APPROVED&sort[submittedAt]=desc

Rapporter innsendt i april 2026 (Prisma gte/lt operatorer):

GET /reports?query[submittedAt][gte]=2026-04-01&query[submittedAt][lt]=2026-05-01

Søk i rapport-tittel (case-insensitive):

GET /reports?query[title][contains]=oslo&query[title][mode]=insensitive

Rapporter som ikke er slettet (deletedAt er null):

GET /reports?query[deletedAt]=NULL

Rapporter for én ansatt, sortert på beløp:

GET /reports?query[accountId]=01HXY...&sort[amount]=desc

Filtrer på relasjon — rapporter i en bestemt avdeling, etter avdelingsnavn:

GET /reports?query[department][name]=Salg

Paginering andre side (rader 50–99):

GET /reports?limit=50&offset=50&sort[createdAt]=desc

Bygg en query trygt i klient (TypeScript):

import qs from 'qs';

const url = `/reports?${qs.stringify({
query: {
status: ['PENDING', 'APPROVED'],
submittedAt: { gte: '2026-04-01', lt: '2026-05-01' },
title: { contains: 'oslo', mode: 'insensitive' },
},
sort: { submittedAt: 'desc' },
limit: 50,
})}`;

Samme mønster gjelder andre findAll-endepunkter — /users, /expenses, /invoices, /flows, osv. Erstatt feltnavnene med dem som er gyldige for ressursen.

Type-coercion (viktig)

Strenger som ser ut som tall, true/false, eller NULL blir automatisk konvertert. Hvis du vil tvinge en streng (f.eks. et organisasjonsnummer), pakk verdien i hermetegn:

query[orgNumber]="987654321"

Det er ett unntak: under nøklene contains, startsWith, endsWith og mode (Prisma string-filtre) blir verdien alltid bevart som streng — ingen coercion. Det betyr at dette virker som forventt:

GET /users?query[email][contains]=123&query[email][mode]=insensitive

Sortering

sort bruker samme bracket-syntaks. Rekkefølgen i URL-en bevares (første nøkkel sorteres først):

GET /users?sort[lastName]=asc&sort[createdAt]=desc

Du kan også sortere på relasjon:

GET /users?sort[organization][displayName]=asc

Bare asc og desc aksepteres.

Tips

  • Bruk klientside-qs.stringify eller URLSearchParams med eksplisitte brackets — ikke konkatener strenger manuelt, særlig ikke for filtere som kan repeteres.
  • Husk URL-encoding for mellomrom og spesialtegn (+ eller %20).
  • Duplikate nøkler i query (f.eks. query[name]=A&query[name]=B) avvises som 400 — bruk komma-listen i stedet.

Feil

Responseformat

Alle feil normaliseres av et globalt exception-filter til samme form:

{
"errorCode": "report_not_pending",
"statusCode": 409,
"message": "Can't approve/reject a report that is not PENDING",
"path": "/organizations/abc/reports/123/approve",
"eventId": "5f1d0e9c..."
}
FeltNår det er satt
errorCodeNår feilen er en kjent, dokumentert forretningsfeil. Stabil nøkkel — bruk denne til å forgrene logikk i klienten. null for generiske feil (validering, auth, 500).
statusCodeAlltid. Speiler HTTP-status.
messageAlltid. Menneskelesbar, kan endres — ikke match på denne.
pathForespørsels-URL-en.
eventIdKun ved interne feil som ble rapportert til Sentry. Oppgi denne til support.

errorCode for forretningsfeil

Forretningsfeil kastes som ErrorCodeException på serveren og resulterer i en stabil errorCode-streng i responsen. Eksempler:

errorCodestatusCodeNår
report_not_pending409Forsøk på å godkjenne/avvise rapport som ikke er PENDING
report_no_expenses400Innsendt rapport mangler utlegg
subscription_feature_not_available429Forsøk på å bruke en feature som ikke er i abonnementet
account_blocked403Brukeren er blokkert i organisasjonen
flow_validation_failed400Flyt-konfigurasjon er ugyldig

Den fulle listen ligger i Swagger på hvert endepunkt (vi annoterer mulige errorCode-verdier per rute) — og er kanonisk i ErrorCode.ts.

Anbefalt klientmønster:

if (!res.ok) {
const body = await res.json();
switch (body.errorCode) {
case 'report_not_pending':
// vis bruker-vennlig melding, ikke retry
break;
case 'subscription_feature_limit_reached':
// tilby oppgradering
break;
default:
// generisk fallback, logg requestId og eventId
}
}

Hvordan rapportere en feil

Når du melder inn en feil til support, ta med:

  1. eventId fra responsbody — hvis satt, peker den rett på Sentry-eventet og er det mest presise vi kan slå opp på.
  2. errorCode, statusCode og path fra responsbody — eller hele body-en.
  3. Tidspunkt (UTC, gjerne med sekundoppløsning) og organisasjons-ID/handle.
  4. Token-type (PAT, SAT, eller bruker-session) — ikke selve tokenet.
  5. Minimal repro: cURL-kall eller payload som utløser feilen.

Eksempel på et godt feilkall i logg på din side:

2026-05-06T10:14:22Z org=abc eventId=5f1d0e9c... errorCode=flow_validation_failed status=400 path=/.../flows/123

Med eventId slår vi opp eventet direkte i Sentry; med tidspunkt + organisasjon + path finner vi det manuelt.

Rate limits

De fleste autentiserte endepunktene har ikke harde grenser, men noen offentlige eller dyre operasjoner gjør:

  • Offentlig attachment-capture (/capture/attachments/*) — 5 kall/min pr. klient.
  • Fingerprint-endepunkter — 30 kall/min.

Når du blir throttlet får du HTTP 429 fra NestJS' throttler-guard. Implementer eksponentiell backoff og ikke retry tettere enn 1 sek.

Korrelasjon mellom forespørsler

API-et emitterer ikke en standard tracing-header (x-request-id/traceparent) i dag — så du kan ikke videresende en bestillings-ID som vi automatisk plukker opp i logger. Det vi har er:

  • eventId i feilresponsen (kun ved interne feil rapportert til Sentry).
  • correlationId som body-felt på enkelte upload-endepunkter (f.eks. POST /capture/attachments, POST /expenses/:id/attachments). Denne er ikke en tracing-ID — den brukes til å koble en opplastet vedleggsfil til en pågående capture-/scan-økt på samme bruker. Sett samme correlationId på alle filer som hører sammen i én capture-flyt.

Hvis du trenger ekte distribuert tracing for en integrasjon, ta kontakt — det er på roadmap.

Brukerinnlogging (web og mobil)

Web og mobil bruker ordinær auth-flyt med refresh-tokens. OAuth-providere som støttes inkluderer Apple, Google, Microsoft Entra ID og Vipps. Du trenger normalt ikke å implementere denne flyten selv — bruk PAT eller SAT for egne integrasjoner.

Reagere på hendelser

Du har to alternativer:

  • SSE (GET /sse/events) — anbefalt for utviklere. Åpne én strøm, lytt på alle hendelser ditt token har tilgang til. Se Sanntid, dypelenker og notifikasjoner.
  • HTTP-forespørsel-handling i Flyt — anbefalt når en organisasjon-admin vil at Utleggsappen kaller en URL ved bestemte rapporthendelser. Konfigureres i UI. Se Flyt og automatisering.

Generelle webhook-abonnement utenom flyt er på roadmap — kontakt support hvis du har et konkret behov.

Se også