# Fejl og statuskoder

Alle fejl har samme form og en HTTP-status, der fortæller, om du skal rette kaldet, opgradere, vente eller prøve igen.

## Fejlformatet

En fejl er altid et JSON-objekt med ét felt, `error`, der beskriver fejlen på engelsk. Forgren på HTTP-statussen, ikke på teksten, som kan blive præciseret over tid.

```json
{ "error": "property not found" }
```

Hvert svar bærer desuden en `X-Request-Id`-header. Angiv den, hvis du kontakter support om et bestemt kald.

## Statuskoder

Tabellen er dannet direkte fra API’ets eget fejlkatalog — den samme liste, som API’et er kontrolleret imod, så en ny fejlstatus ikke kan tages i brug uden at stå her. Kataloget kan også hentes maskinlæsbart og uden nøgle fra [`GET /v1/errors`](https://grundfast.dk/docs/api/fejlkatalog), med en stabil `type` pr. fejltype.

| Status | Betydning i Grundfast | Prøv igen? |
| --- | --- | --- |
| 400 | Ugyldig forespørgsel: forkert BFE- eller UUID-format, en manglende eller for kort parameter, et koordinat uden for Danmark, en ugyldig `asOf`, `srid`, `cirkel` eller `polygon`, eller et body, der ikke passer til skemaet. | Nej — ret kaldet. |
| 401 | Nøglen mangler, er ugyldig, udløbet eller tilbagekaldt. Svaret bærer en `WWW-Authenticate`-header, der beskriver formatet. | Nej — ret kaldet. |
| 402 | Funktionen er ikke med i din plan. Batch og webhooks kræver Pro; eksport og historik (`asOf` og `/historik`) kræver Scale. Beskeden nævner den plan, der skal til. | Nej — ret kaldet. |
| 403 | Kontoen er suspenderet, eller nøglen er en administrationsnøgle, der ikke gælder for data-endpoints. Et kald med cookie-session fra et andet site til et konto-endpoint (login, nøgler, betaling) afvises. Gælder ikke kald med API-nøgle. | Nej — ret kaldet. |
| 404 | Ressourcen findes ikke: ukendt BFE, UUID, CVR-nummer, kode eller sti. | Nej — ret kaldet. |
| 409 | Ressourcen er ikke i en tilstand, der tillader kaldet — typisk en eksport, der ikke er færdig endnu, når du henter filen. | Ja, når forudsætningen er opfyldt. |
| 413 | Request-body over 1 MB. Del kaldet op. | Nej — ret kaldet. |
| 415 | Et konto-endpoint fik et body, der ikke er `application/json`. Send `Content-Type: application/json`. | Nej — ret kaldet. |
| 422 | Jordstykket kunne findes, men bærer ingen ejerlavskode og intet matrikelnummer, så kortlægningen kan ikke slås op på matriklen. Slå op på koordinatet i stedet. | Nej — ret kaldet. |
| 429 | Burst-grænsen pr. minut for din plan er nået. `Retry-After` siger, hvornår vinduet åbner igen. Månedens kvote er brugt (eller sandbox-loftet for en testnøgle). Svaret bærer `X-Quota-*`-headere, og `Retry-After` peger på næste måned — prøv ikke igen automatisk. For mange forsøg med ugyldige nøgler fra samme IP-adresse. Ret nøglen og vent det antal sekunder, `Retry-After` angiver. | `rate_limited`: Ja, efter `Retry-After`. `quota_exceeded`: Nej — ret kaldet. `auth_throttled`: Ja, efter `Retry-After`. |
| 500 | En uventet fejl hos os. Den er logget med kaldets `X-Request-Id`; angiv det, hvis du kontakter support. | Ja, med backoff. |
| 502 | Kilden (fx Datafordeler) svarede med fejl, efter vi har prøvet igen, og der var ingen brugbar cachet kopi. | Ja, med backoff. |
| 503 | Kilden er ikke tilgængelig i øjeblikket, endpointet er ikke sat op i dette miljø (fx adresseindekset eller rumlige filtre), eller rate-limiteren er midlertidigt utilgængelig. Energimærke: ejendommen er ikke dækket endnu. Det er ikke det samme som "intet energimærke" — det ville være en 404. | Ja, med backoff. |
| 504 | Svaret tog længere end tidsgrænsen på ca. 20 sekunder. Arbejdet fortsætter i baggrunden, så et nyt kald er ofte hurtigt. | Ja, med backoff. |

## Hvilke fejl tæller

- **5xx og 504 tæller ikke.** Et kald, der ender i en serverfejl eller tidsgrænsen, lægges tilbage i månedens kvote.
- **4xx tæller med i kvoten.** Et kald med en ugyldig BFE eller et ukendt nummer bruger en enhed, ligesom et vellykket kald.
- **Kun vellykkede svar faktureres.** Overforbrug på betalte planer beregnes ud fra svar under `400`.
- **402 og 429 tæller ikke.** Plan-tjekket og rate limits afviser kaldet, før det måles.

## Hvornår du skal prøve igen

- **429 fra burst-grænsen:** vent det antal sekunder, `Retry-After` angiver, og prøv igen.
- **429 fra månedens kvote:** svaret bærer også `X-Quota-*`-headere, og `Retry-After` peger på næste måned. Prøv ikke igen automatisk.
- **502, 503 og 504:** prøv igen med eksponentiel backoff og jitter, fx 0,5 s, 1 s, 2 s. Stop efter nogle få forsøg.
- **400, 401, 402, 403 og 404:** prøv aldrig igen uden at ændre kaldet. Svaret bliver det samme.

```ts
const RETRYABLE = new Set([429, 502, 503, 504]);

export async function grundfastGet<T>(path: string, maxAttempts = 4): Promise<T> {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://api.grundfast.dk${path}`, {
      headers: { Authorization: `Bearer ${process.env.GRUNDFAST_API_KEY}` },
    });
    if (res.ok) return (await res.json()) as T;

    // Månedens kvote er en 429 med X-Quota-*-headere: den forsvinder ikke ved at vente.
    const quotaExhausted = res.status === 429 && res.headers.has('X-Quota-Remaining');
    if (!RETRYABLE.has(res.status) || quotaExhausted || attempt >= maxAttempts) {
      const body = (await res.json().catch(() => ({}))) as { error?: string };
      throw new Error(`Grundfast ${res.status}: ${body.error ?? res.statusText}`);
    }

    const retryAfter = Number(res.headers.get('Retry-After'));
    const delayMs =
      retryAfter > 0 ? retryAfter * 1000 : 2 ** (attempt - 1) * 500 + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}

// const ejendom = await grundfastGet('/v1/bbr/ejendom/5651067');
```

## Fejl i SDK’et

[TypeScript-SDK’et](https://grundfast.dk/docs/sdk) kaster en `GrundfastError` for ethvert svar uden for 2xx. Den har `status` (HTTP-statussen), `message` (teksten fra `error`) og `retryAfter` (sekunder fra `Retry-After`, når headeren findes). En timeout bliver til `status` 408.

```ts
import { GrundfastClient, GrundfastError } from '@grundfast/sdk';

const gf = new GrundfastClient({ apiKey: 'gf_live_...' });

try {
  const ejendom = await gf.ejendom(5651067);
  console.log(ejendom.antal_bygninger);
} catch (err) {
  if (err instanceof GrundfastError && err.status === 404) {
    console.log('Ukendt BFE');
  } else if (err instanceof GrundfastError && err.status === 402) {
    console.log('Kræver en højere plan:', err.message);
  } else {
    throw err;
  }
}
```

SDK’et prøver selv igen ved 408, 500, 502 og 503 og ved burst-429, med backoff og op til to ekstra forsøg som standard (`maxRetries`). Det prøver aldrig igen ved månedens kvote-429, og en eksport oprettes aldrig to gange. `504` prøves ikke igen automatisk, så håndtér den selv, hvis du vil.
