---
title: "API og webhooks"
slug: "api"
canonical: "https://konradoffice.no/dokumentasjon/api"
description: "Et versjonert REST-API for å lese og skrive selskapets data, og webhooks som varsler deg når noe skjer. Alle endepunkter er dokumentert her."
language: "nb-NO"
---

# API og webhooks

Et versjonert REST-API for å lese og skrive selskapets data, og webhooks som varsler deg når noe skjer. Alle endepunkter er dokumentert her.

## Komme i gang

1. Gå til **Innstillinger → Integrasjoner**
2. Opprett et token med bare de tilgangene integrasjonen trenger
3. Velg levetid — tolv måneder er standard
4. Kopier tokenet — det vises bare én gang

Send tokenet som `Authorization`-header på hvert kall:

```
curl https://konradoffice.no/api/v1/contacts \
  -H "Authorization: Bearer kon_xxxxxxxxxxxxxxxx" \
  -H "Accept: application/json"
```

Tokenet avgjør hvilket selskap kallet handler på vegne av. Det finnes ingen parameter for å bytte selskap, og et token når aldri data utenfor sitt eget.

Bare eier og daglig leder kan opprette tokens. Alle svar er JSON, og alle datoer er ISO 8601.

## Alle endepunkter

Alt ligger under `/api/v1`. Versjonen står i stien fra første dag, så en endring som bryter noe er noe man velger å ta i bruk, ikke noe man våkner til.

                            MetodeStiTilgangGjør

                                GET`/api/v1/contacts``contacts:read`Liste kontakter
                                GET`/api/v1/contacts/{id}``contacts:read`Hente én kontakt
                                POST`/api/v1/contacts``contacts:write`Opprette kontakt
                                GET`/api/v1/products``products:read`Liste produkter
                                GET`/api/v1/products/{id}``products:read`Hente ett produkt
                                POST`/api/v1/products``products:write`Opprette produkt
                                GET`/api/v1/invoices``invoices:read`Liste fakturaer
                                GET`/api/v1/invoices/{id}``invoices:read`Hente én faktura med linjer

Disse tilgangene finnes også, men har ingen endepunkter ennå: `vouchers:read` og `webhooks:manage`.

## Kontakter

#### GET /api/v1/contacts

Krever `contacts:read`. Parametre:

                            ParameterBetydning

                                `type``customer`, `supplier` eller `both`
                                `search`Fritekst mot navn
                                `updated_since`Bare kontakter endret etter dette tidspunktet
                                `per_page`1–100, standard 25

```
{
  "data": [
    {
      "id": 12,
      "contact_number": "CON20260012",
      "type": "customer",
      "is_private": false,
      "name": "Bakeriet AS",
      "organization_number": "123456789",
      "email": "post.no",
      "phone": "22334455",
      "address": {
        "street": "Storgata 1",
        "postal_code": "0155",
        "city": "Oslo",
        "country": "NO"
      },
      "payment_terms_days": 14,
      "is_active": true,
      "created_at": "2026-08-30T09:15:00+02:00",
      "updated_at": "2026-08-30T09:15:00+02:00"
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta": { "current_page": 1, "per_page": 25, "total": 143 }
}
```

#### GET /api/v1/contacts/{id}

Krever `contacts:read`. Samme felter, uten `data`-liste. Kontakter i andre selskaper gir 404.

#### POST /api/v1/contacts

Krever `contacts:write`.

                            FeltKravMerknad

                                `name`PåkrevdMaks 255 tegn
                                `type`Påkrevd`customer`, `supplier` eller `both`
                                `organization_number`ValgfriNøyaktig ni siffer
                                `email`ValgfriGyldig e-postadresse
                                `phone`ValgfriMaks 30 tegn
                                `address`, `postal_code`, `city`Valgfri
                                `country`ValgfriTo bokstaver, standard `NO`
                                `payment_terms_days`Valgfri0–365, standard 14

```
curl -X POST https://konradoffice.no/api/v1/contacts \
  -H "Authorization: Bearer kon_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Bakeriet AS","type":"customer","organization_number":"123456789"}'
```

Svarer **201** med den opprettede kontakten. Kontaktnummer tildeles automatisk. Utløser webhooken `contact.created`.

## Produkter

#### GET /api/v1/products

Krever `products:read`. Parametre:

                            ParameterBetydning

                                `search`Fritekst mot navn
                                `active_only``true` utelater deaktiverte produkter
                                `updated_since`Bare produkter endret etter dette tidspunktet
                                `per_page`1–100, standard 25

```
{
  "data": [
    {
      "id": 7,
      "sku": "KAFFE-500",
      "name": "Kaffe 500 g",
      "description": "Mørkbrent",
      "price": 129.00,
      "cost_price": 74.50,
      "is_stocked": true,
      "is_active": true,
      "created_at": "2026-08-30T09:15:00+02:00",
      "updated_at": "2026-08-30T09:15:00+02:00"
    }
  ]
}
```

#### GET /api/v1/products/{id}

Krever `products:read`.

#### POST /api/v1/products

Krever `products:write`.

                            FeltKravMerknad

                                `name`PåkrevdMaks 255 tegn
                                `price`PåkrevdTall, minst 0
                                `sku`ValgfriMå være ledig i selskapet
                                `description`ValgfriMaks 1000 tegn
                                `cost_price`ValgfriTall, minst 0
                                `is_stocked`ValgfriOm produktet lagerføres

Svarer **201** med det opprettede produktet.

## Fakturaer

#### GET /api/v1/invoices

Krever `invoices:read`. Sortert med nyeste fakturadato først.

                            ParameterBetydning

                                `contact_id`Bare fakturaer til denne kontakten
                                `unpaid_only``true` gir bare ubetalte
                                `from`, `to`Avgrenser på fakturadato
                                `per_page`1–100, standard 25

#### GET /api/v1/invoices/{id}

Krever `invoices:read`. Denne tar med fakturalinjene; listen gjør det ikke.

```
{
  "data": {
    "id": 42,
    "invoice_number": "F-2026-0042",
    "type": "invoice",
    "contact_id": 12,
    "invoice_date": "2026-06-15",
    "due_date": "2026-07-15",
    "amounts": {
      "subtotal": 10000.00,
      "discount_total": 0.00,
      "vat_total": 2500.00,
      "total": 12500.00,
      "balance": 12500.00
    },
    "sent_at": "2026-06-15T10:02:00+02:00",
    "paid_at": null,
    "lines": [
      {
        "id": 91,
        "description": "Konsulenttimer",
        "quantity": 10.0,
        "unit": "time",
        "unit_price": 800.00,
        "discount_percent": 0.0,
        "vat_percent": 25.0,
        "line_total": 8000.00,
        "line_vat": 2000.00
      }
    ],
    "created_at": "2026-06-15T09:58:00+02:00"
  }
}
```

`balance` er utestående beløp; null betyr betalt.

**Fakturaer kan foreløpig bare leses.** Å opprette en faktura berører nummerserie, MVA og hovedbok, og gjøres ikke fra et tynt API-kall før skrivestien er noe vi kan stå inne for.

## Paginering og synkronisering

Lister er paginert. Hvert svar har `links` og `meta`:

```
"meta": { "current_page": 2, "last_page": 6, "per_page": 25, "total": 143 }
```

Følg `links.next` til den er `null`. `per_page` kan settes opp til 100 — ber du om mer, får du 100.

For løpende synkronisering, bruk `updated_since` med tidspunktet for forrige vellykkede kjøring i stedet for å hente alt på nytt:

```
GET /api/v1/contacts?updated_since=2026-08-30T09:00:00%2B02:00&per_page=100
```

## Levetid og tilbakekalling

Et token får levetid når det opprettes: 3, 12 eller 24 måneder, eller uten utløp. Standard er tolv måneder.

Et token uten utløp er et token ingen husker å tilbakekalle når integrasjonen legges ned. Velg det bare når noe faktisk krever det, og noter hvorfor.

Et utløpt token slutter å virke av seg selv og svarer **401**, akkurat som et tilbakekalt. Oversikten viser når hvert token utløper, og hvilke som står uten utløp.

**Tokenet vises bare én gang.** Bare en hash lagres, så det kan ikke hentes fram igjen — heller ikke av oss. Mister du det, tilbakekall det og lag et nytt.

Tilbakekalling virker umiddelbart. Integrasjoner som bruker tokenet slutter å virke ved neste kall, så lag heller ett token per integrasjon enn ett felles.

## Feilkoder

KodeBetyr

                                **401**Tokenet mangler, er ukjent, tilbakekalt eller utløpt
                                **403**Tokenet mangler tilgangen kallet krever
                                **404**Ressursen finnes ikke i selskapet tokenet tilhører
                                **422**Ugyldig innhold
                                **429**For mange kall

401 skiller ikke mellom ukjent, tilbakekalt og utløpt. Å si hvilket det er, hjelper en angriper med å kartlegge gyldige tokens.

403 sier hvilken tilgang som mangler:

```
{ "message": "Tokenet mangler tilgangen «invoices:read».", "required_scope": "invoices:read" }
```

422 gir feltvise meldinger:

```
{ "message": "...", "errors": { "name": ["Navn er påkrevd."] } }
```

## Kallgrenser

120 kall per minutt per token. Kall uten token er begrenset hardere, siden avsenderen ikke er identifisert.

Grensen går per token, ikke per adresse: flere integrasjoner deler ofte én server, og en IP-basert grense ville latt en travel nabo strupe alle andre.

Ved 429 sier `Retry-After` hvor lenge du bør vente.

## Webhooks

Opprett et endepunkt under **Innstillinger → Integrasjoner** og velg hvilke hendelser du vil høre om. URL-en må være https.

                            HendelseUtløses når

                                `contact.created`En kontakt opprettes
                                `contact.updated`En kontakt endres
                                `invoice.created`En faktura opprettes
                                `invoice.sent`En faktura sendes
                                `invoice.paid`En faktura registreres betalt
                                `product.created`Et produkt opprettes
                                `voucher.received`Et bilag mottas

Alle sju utløses. `invoice.sent` og `invoice.paid` kommer på overgangen — det øyeblikket feltet går fra tomt til satt — så du får dem én gang, ikke på nytt hver gang noe annet på fakturaen endres.

`contact.updated` utløses bare når noe en integrasjon bryr seg om er endret: navn, organisasjonsnummer, e-post, telefon, type, adresse, betalingsbetingelser eller aktiv-status. Nyttelasten sier hvilke felter det gjaldt, i `changed`. Interne oppdateringer som siste kontaktdato varsler ikke.

Hendelsene kommer fra selve datalaget, ikke fra enkelte skjermbilder, så de utløses uansett om posten kom fra grensesnittet, API-et, en import eller AI-assistenten.

Hvert kall er en POST med JSON:

```
{
  "event": "invoice.sent",
  "created_at": "2026-08-30T09:15:00+02:00",
  "data": {
    "id": 42,
    "invoice_number": "F-2026-0042",
    "contact_id": 12,
    "total": 12500.00,
    "due_date": "2026-07-15",
    "sent_at": "2026-08-30T09:15:00+02:00"
  }
}
```

## Verifisere signaturen

Hvert kall bærer disse headerne:

                            HeaderInnhold

                                `X-Konrad-Signature`HMAC-SHA256 av `tidsstempel.body`
                                `X-Konrad-Timestamp`Unix-tid da kallet ble laget
                                `X-Konrad-Event`Hendelsen, for ruting
                                `X-Konrad-Delivery`Leverings-ID, for å kjenne igjen gjenforsøk

```
$forventet = hash_hmac('sha256', $timestamp . '.' . $body, $hemmelighet);

if (! hash_equals($forventet, $signatur)) {
    abort(400);
}

if (abs(time() - (int) $timestamp) > 300) {
    abort(400);
}
```

Tidsstempelet er en del av det signerte, så et fanget kall kan ikke spilles av senere. Avvis alt eldre enn noen minutter, og sammenlign i konstant tid — en signaturkontroll som lekker timing er ingen kontroll.

## Levering og gjenforsøk

Svarer du 2xx, regnes leveringen som fullført. Alt annet prøves på nytt med økende mellomrom: 10 sekunder, 1 minutt, 5 minutter, 15 minutter, 1 time.

Et endepunkt som feiler 15 ganger på rad slås av, og du ser det i oversikten. En URL som har vært død i to uker skal slutte å lage arbeid.

Leveringen skjer på kø, så et tregt endepunkt gjør ikke faktureringen treg.

**Svar raskt.** Ta imot kallet, legg det i din egen kø, og svar 200. Gjør du tungt arbeid før du svarer, får du gjenforsøk du ikke trenger.

Bruk `X-Konrad-Delivery` til å kjenne igjen gjenforsøk, så samme hendelse ikke behandles to ganger.

## Tips

- Lag ett token per integrasjon, ikke ett felles — da kan du tilbakekalle én uten å stoppe resten
- Gi bare tilgangene som trengs. En nettbutikk som sender ordrer skal ikke kunne lese lønn
- Bruk `updated_since` i stedet for å hente alt på nytt
- Tokens kan ikke hentes fram igjen. Mister du et, tilbakekall det og lag et nytt
- Et token som ikke har vært brukt vises med «Aldri brukt» — nyttig for å finne integrasjoner som aldri kom i gang
