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
Listen inkluderer lastUsedAt – tidspunktet for siste vellykkede autentisering med tokenen, oppdatert med inntil 15 minutters forsinkelse. Verdien er null for tokens som ikke er brukt siden feltet ble innført.
Sikkerhetsregler
- Lagre PAT som en hemmelighet (env var, secret store) – aldri i kildekode eller logger.
- Sett alltid
expiresAtder det er praktisk. - Tilbakekall ubrukte tokens – bruk
lastUsedAtfor å finne dem. - 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 har prefiks uas_.
Opprett og administrer en tjenestekonto i webdashbordet
Organisasjons-administratorer oppretter og administrerer org-scopede tjenestekontoer selvbetjent:
- Logg inn på dashbordet.
- Gå til Innstillinger → Utvikler → Tjenestekontoer.
- Klikk Ny tjenestekonto, gi den et navn og velg tilgangsnivå (se under).
- Åpne tjenestekontoen og lag en tilgangstoken under fanen for tokens.
- Kopier tokenen umiddelbart – den vises kun én gang.
Tjenestekontoen kan omdøpes eller slettes, og enkelttokens kan tilbakekalles uten å slette kontoen.
Selvbetjeningen dekker org-scopede tjenestekontoer. En globalt-scopet SAT (for partnere/systemintegratorer med cross-tenant behov) opprettes ikke selvbetjent – ta kontakt på chat på utleggsappen.no og oppgi organisasjon, bruksområde, ønsket tilgang og en sikker leveringskanal for tokenen.
SAT-administrasjonsendepunktene under /organizations/:organizationId/service-accounts finnes, men er i dag interne og ikke i den offentlige Swagger-tag-listen – bruk dashbordet til å administrere org-scopede tjenestekontoer. Samme lastUsedAt-felt som for PAT-er finnes også på SAT-er, og kan brukes til å vurdere om en tjenestekonto fortsatt er i bruk.
Tilgangsnivå for org-scopede tjenestekontoer
Hver tjenestekonto har ett av to grunnleggende tilgangsnivåer, valgt ved opprettelse og synlig i dashbordet:
| Nivå | Tilgang |
|---|---|
| Admin | Les rapporter; les og oppdater fakturaer; full administrasjon av avdelinger, kategorier og prosjekter; les og administrer kontoer/organisasjon (inkl. provisjonering) |
| Read-only | Kun lesetilgang til rapporter, fakturaer, kategorier, avdelinger og prosjekter – ingen skrivetilgang, og ingen tilgang til kontoer/organisasjon |
Utover grunnivået kan en organisasjons-admin tildele tjenestekontoen eksisterende egendefinerte roller fra tilgangspanelet i dashbordet, på samme måte som for brukere – nyttig for å gi presis tilgang uten å gå til Admin-nivået. Panelet viser også kontoens effektive tilgang (roller og tillatelsesnøkler).
SAT-er kan ikke opprette, endre, godkjenne eller slette rapporter, uansett tilgangsnivå eller tildelte roller. En SAT opptrer aldri som rapportinnleverer.
En globalt-scopet SAT kan lese brukere, kontoer og organisasjoner på tvers av tenants – brukes av partnere og systemintegratorer med cross-tenant behov.
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.
Sjekke effektiv tilgang
For å lese hvilke roller og tillatelsesnøkler som gjelder for den autentiserte brukeren eller service-kontoen:
GET /users/current/permissions
Endepunktet er uprivilegert – en bruker eller SAT leser alltid bare sin egen effektive tilgang. Responsen inneholder roles (liste med rolleobjekter, inkl. scope og scopeId) og permissions (deduplisert, sortert liste av tillatelsesnøkler):
{
"roles": [
{
"id": "r1",
"name": "Approver",
"scope": "ORGANIZATION",
"scopeId": "org-abc",
"type": "CUSTOM"
}
],
"permissions": ["report.approve.org", "report.read.org"]
}
Bruk dette endepunktet til å implementere klient-side tilgangskontroll (can()-sjekker) i integrasjoner uten å hardkode rollenavn.
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 forventet:
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 |
account_deleted | 403 | Kontoen er myk-slettet i organisasjonen og kan ikke tildeles ressurser |
flow_validation_failed | 400 | Flyt-konfigurasjon er ugyldig |
enterprise_account_role_above_actor_privilege | 403 | Ønsket enterprise-rolle er mer privilegert enn aktørens egen rolle |
enterprise_invitation_role_above_actor_privilege | 403 | Ønsket invitasjonsrolle er mer privilegert enn aktørens enterprise-rolle |
invitation_creator_no_longer_authorized | 403 | Den som opprettet invitasjonen har ikke lenger nødvendig tilgang til å tildele rollen |
role_not_found | 404 | Rollen finnes ikke |
system_role_immutable | 403 | Forsøk på å endre eller slette en systemrolle via rolle-API-et |
role_cross_tenant | 403 | Rollen tilhører en annen organisasjon enn den i forespørselen |
report_deleted | 409 | Forsøk på å endre en myk-slettet rapport. Rapporten er fortsatt lesbar (GET gir 200, ikke 404) – gjenopprett den først for å kunne redigere videre |
statutory_rate_not_overridable | 400 | Forsøk på å sette en organisasjonsspesifikk verdi for en lovbestemt trekkfri sats (kjøregodtgjørelse eller diett) |
drive_rate_unavailable_for_year | 422 | Kjøregodtgjørelse mangler en gjeldende statlig sats for kjøreåret |
role_assignment_scope_mismatch | 400 | Bulk-tilordning støtter bare ORGANIZATION-scope roller |
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. - Kontrollfunn (
GET /reports/:id/findings) – 30 kall/min. Triage (dismiss/confirm/unresolve) – 20 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. Støttede OAuth-leverandører inkluderer Apple, Google, Microsoft Entra ID og Vipps. Du trenger normalt ikke å implementere denne flyten selv – bruk PAT eller SAT for egne integrasjoner.
AI-assistenter (MCP)
En bruker kan også koble en AI-assistent som Claude til API-et via OAuth og MCP (Model Context Protocol)
på https://api.utlegg.app/mcp. Tilgangen er skrivebeskyttet og begrenset til brukerens egne roller. Se
AI-assistenter og MCP for hvordan tilkobling, samtykke og
tilbakekalling fungerer.
Ekstern tilgang for regnskapsfører eller rådgiver
En organisasjons-Admin kan gi en ekstern praktiker (typisk en regnskapsfører eller controller som allerede har en Utleggsappen-brukerkonto fra en annen organisasjon) midlertidig tilgang til egen organisasjon – uten å opprette et vanlig medlemskap eller sende invitasjon. Dette gjøres i dag kun via API-et; det finnes ikke et eget punkt for dette i dashbordet.
curl -X POST https://api.utlegg.app/organizations/{organizationId}/access-grants \
-H "Authorization: Bearer <admin-PAT>" \
-H "Content-Type: application/json" \
-d '{"userId": "<bruker-uuid>", "roleId": "<rolle-id>", "expiresInDays": 90}'
- Bruker: mottakeren må ha en eksisterende Utleggsappen-brukerkonto – det sendes ingen invitasjon på e-post.
- Rolle: du velger selv hvilken organisasjonsrolle (system- eller egendefinert) praktikeren skal ha – fra lesetilgang til full
Admin. Å giACCOUNT_OWNER-rollen krever at du selv har den rollen i organisasjonen. - Varighet: standard 90 dager, kan settes opp til maks 365 dager (
expiresInDays). Tilgangen utløper automatisk – det finnes ikke en_EXPIRED-hendelse, kun en stille filtrering ved lesing. - Forny: kall samme endepunkt på nytt med samme bruker, organisasjon og rolle – dette forlenger utløpsdatoen fremfor å opprette en ny tilgang.
- Tilbakekall:
DELETE /organizations/{organizationId}/access-grants/:id. - Oversikt:
GET /organizations/{organizationId}/access-grantsviser organisasjonens egne tildelinger; en praktiker kan selv seGET /users/current/access-grantsfor tilganger på tvers av organisasjoner.
Alt knyttet til denne tilgangstypen audit-logges (tildelt, fornyet, tilbakekalt) – se Sporbarhet, sikkerhet og kontroll.
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.