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

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 expiresAt der det er praktisk.
  • Tilbakekall ubrukte tokens – bruk lastUsedAt for å 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:

  1. Logg inn på dashbordet.
  2. Gå til Innstillinger → Utvikler → Tjenestekontoer.
  3. Klikk Ny tjenestekonto, gi den et navn og velg tilgangsnivå (se under).
  4. Åpne tjenestekontoen og lag en tilgangstoken under fanen for tokens.
  5. Kopier tokenen umiddelbart – den vises kun én gang.

Tjenestekontoen kan omdøpes eller slettes, og enkelttokens kan tilbakekalles uten å slette kontoen.

Globalt scope må fortsatt bestilles

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
AdminLes rapporter; les og oppdater fakturaer; full administrasjon av avdelinger, kategorier og prosjekter; les og administrer kontoer/organisasjon (inkl. provisjonering)
Read-onlyKun 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.

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 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.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
account_deleted403Kontoen er myk-slettet i organisasjonen og kan ikke tildeles ressurser
flow_validation_failed400Flyt-konfigurasjon er ugyldig
enterprise_account_role_above_actor_privilege403Ønsket enterprise-rolle er mer privilegert enn aktørens egen rolle
enterprise_invitation_role_above_actor_privilege403Ønsket invitasjonsrolle er mer privilegert enn aktørens enterprise-rolle
invitation_creator_no_longer_authorized403Den som opprettet invitasjonen har ikke lenger nødvendig tilgang til å tildele rollen
role_not_found404Rollen finnes ikke
system_role_immutable403Forsøk på å endre eller slette en systemrolle via rolle-API-et
role_cross_tenant403Rollen tilhører en annen organisasjon enn den i forespørselen
report_deleted409Forsø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_overridable400Forsøk på å sette en organisasjonsspesifikk verdi for en lovbestemt trekkfri sats (kjøregodtgjørelse eller diett)
drive_rate_unavailable_for_year422Kjøregodtgjørelse mangler en gjeldende statlig sats for kjøreåret
role_assignment_scope_mismatch400Bulk-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:

  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.
  • 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:

  • 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. 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. Å gi ACCOUNT_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-grants viser organisasjonens egne tildelinger; en praktiker kan selv se GET /users/current/access-grants for 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.

Se også​