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:
| Prefiks | Type | Brukes for |
|---|---|---|
ua_ | Personal Access Token (PAT) | Personlige integrasjoner og utviklerbruk |
uas_ | Service Access Token (SAT) | Maskin-til-maskin (organisasjon eller globalt) |
| (JWT) | Bruker-session | Web/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
- Logg inn på dashbordet.
- Gå til Profil → Tokens (eller
Innstillinger → Personlige tokens). - Klikk Ny token, gi den et navn og eventuelt en utløpsdato.
- 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 PATsDELETE /users/pat/:id— tilbakekall en PAT
Sikkerhetsregler
- Lagre PAT som en hemmelighet (env var, secret store) — aldri i kildekode eller logger.
- Sett alltid
expiresAtder 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-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.
| Parameter | Type | Beskrivelse |
|---|---|---|
query | object (bracket-syntaks) | Filtre, mappes direkte til Prisma where |
sort | object (bracket-syntaks) | Sorteringsnøkler, mappes til Prisma orderBy |
limit | number ≥ 1 | Max rader pr. side |
offset | number ≥ 0 | Offset 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.stringifyellerURLSearchParamsmed 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..."
}
| Felt | Når det er satt |
|---|---|
errorCode | Når feilen er en kjent, dokumentert forretningsfeil. Stabil nøkkel — bruk denne til å forgrene logikk i klienten. null for generiske feil (validering, auth, 500). |
statusCode | Alltid. Speiler HTTP-status. |
message | Alltid. Menneskelesbar, kan endres — ikke match på denne. |
path | Forespørsels-URL-en. |
eventId | Kun 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:
errorCode | statusCode | Når |
|---|---|---|
report_not_pending | 409 | Forsøk på å godkjenne/avvise rapport som ikke er PENDING |
report_no_expenses | 400 | Innsendt rapport mangler utlegg |
subscription_feature_not_available | 429 | Forsøk på å bruke en feature som ikke er i abonnementet |
account_blocked | 403 | Brukeren er blokkert i organisasjonen |
flow_validation_failed | 400 | Flyt-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:
eventIdfra responsbody — hvis satt, peker den rett på Sentry-eventet og er det mest presise vi kan slå opp på.errorCode,statusCodeogpathfra responsbody — eller hele body-en.- Tidspunkt (UTC, gjerne med sekundoppløsning) og organisasjons-ID/handle.
- Token-type (PAT, SAT, eller bruker-session) — ikke selve tokenet.
- 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:
eventIdi feilresponsen (kun ved interne feil rapportert til Sentry).correlationIdsom 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 sammecorrelationIdpå 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.