# Fejlformatet: DAWA-stier og /api/v1

> Læs fejlsvar fra begge dele af API'et med én hjælpefunktion, og reager på den stabile code i stedet for fejlteksten.
>
> https://danadresse.dk/guides/fejlformat · verified 2026-11-02
API'et har to fejlformater. De DAWA-kompatible stier (`/adresser`, `/autocomplete`, `/postnumre` og resten uden `/api/v1`) svarer i DAWA's RFC 7807-format, så eksisterende DAWA-klienter kan læse fejlene. Vores egne endpoints under `/api/v1` bruger en `error`-konvolut. Begge formater har et felt `code`, som du kan bygge din fejlhåndtering på.

## DAWA-stier: type, title, details og code

```json id=dawa-401 run=no
{
  "type": "UnauthorizedError",
  "title": "The provided API key is invalid or revoked.",
  "details": null,
  "code": "invalid_api_key"
}
```

- `type` er DAWA's fejltype. Den afhænger kun af statuskoden.
- `title` er en tekst til mennesker. Den kan blive omformuleret, så brug den ikke i `if`-sætninger.
- `details` er `null`, et objekt eller en liste. Ved valideringsfejl er det en liste over de parametre, der var forkerte.
- `code` findes ikke i DAWA. Vi har tilføjet den, så du kan skelne fx `key_blocked` fra `invalid_api_key` uden at læse teksten. Felter, en DAWA-klient ikke kender, bliver normalt ignoreret, så tilføjelsen bryder ikke eksisterende kode.

| Status | `type` |
|---|---|
| 400 | `QueryParameterFormatError` |
| 401 | `UnauthorizedError` |
| 403 | `ForbiddenError` |
| 404 | `ResourceNotFoundError` |
| 429 | `RateLimitExceededError` |
| 500 | `InternalServerError` |
| 410 | `GoneError` |
| 502 | `BadGatewayError` |
| 503 | `ServiceUnavailableError` |

En valideringsfejl på en DAWA-sti har altid status 400 og `code` `invalid_query_parameter`. `details` viser hvilken parameter og hvorfor:

```json id=dawa-400 run=no
{
  "type": "QueryParameterFormatError",
  "title": "Invalid query parameter",
  "details": [
    {
      "type": "less_than_equal",
      "loc": ["query", "per_side"],
      "msg": "Input should be less than or equal to 1000",
      "input": "5000",
      "ctx": {"le": 1000}
    }
  ],
  "code": "invalid_query_parameter"
}
```

## /api/v1: error-konvolutten

Fejl fra `/api/v1` har objektet `error` med `code`, `message` og eventuelt `details`. Når fejlen kommer fra API'ets egen kontrol (nøgle, kvote, adgang), står det samme objekt også under `detail`, så klienter skrevet til FastAPI's standardformat stadig virker:

```json id=v1-403 run=no
{
  "detail": {
    "error": {
      "code": "browser_key_scope",
      "message": "Domain-locked browser keys cover address lookups only. Use a server-side key for enrich, datavask/parse and stats."
    }
  },
  "error": {
    "code": "browser_key_scope",
    "message": "Domain-locked browser keys cover address lookups only. Use a server-side key for enrich, datavask/parse and stats."
  }
}
```

Valideringsfejl på `/api/v1` har status 422 og kun `error`-objektet:

```json id=v1-422 run=no
{
  "error": {
    "code": "validation_error",
    "message": "Invalid input",
    "details": [
      {"type": "missing", "loc": ["query", "betegnelse"], "msg": "Field required", "input": null}
    ]
  }
}
```

Læs altid `error.code`. Det findes i alle `/api/v1`-fejl, mens `detail` kun findes i nogle af dem.

## Formatet gælder alle fejl

Formatet følger stien, uanset hvor fejlen opstår:

- **500** svarer med `type` `InternalServerError` og `code` `internal_error` på DAWA-stierne og med `{"error":{"code":"internal_error",…}}` på `/api/v1`.
- **403 med `code` `forbidden`** kommer fra vores misbrugsfilter, før kaldet når API'et. Den har også DAWA-formatet (`type` `ForbiddenError`) på DAWA-stierne og `error`-konvolutten på `/api/v1`.
- **404 på en sti, der ikke findes**, har `code` `route_not_found` i begge formater.

Alle fejlsvar fra API'et har `Access-Control-Allow-Origin: *`, så JavaScript i en browser kan læse status og body i stedet for at få en CORS-fejl.

Kun **502 og 504** falder uden for. De kommer fra proxyen foran API'et og har en HTML-side som body. Din kode skal tåle et svar, der ikke er JSON.

## Koderne

| `code` | Status | Betydning | Hvad du gør |
|---|---|---|---|
| `missing_api_key` | 401 | Ingen nøgle i header eller URL | Send `X-Api-Key`. Se [Fejl 401](/guides/fejl-401) |
| `invalid_api_key` | 401 | Nøglen findes ikke eller er slettet | Tjek stavning og miljøvariabel |
| `account_suspended` | 403 | Kontoen er spærret | Skriv til os via [support](/support) |
| `key_blocked` | 403 | Nøglen er spærret som lækket | Lav en ny nøgle eller lås den til domæner |
| `origin_not_allowed` | 403 | Browsernøgle brugt fra et andet domæne | Tilføj domænet eller brug en servernøgle |
| `browser_key_scope` | 403 | Browsernøgle på enrich/parse/stats | Kald fra din server med en servernøgle |
| `sandbox_scope` | 403 | Testnøgle på enrich/parse/stats | Brug en live-nøgle |
| `ip_not_allowed` | 403 | IP-låst nøgle brugt fra en anden IP | Tilføj IP'en på nøglen |
| `forbidden` | 403 | Misbrugsfilteret har blokeret din IP | Skriv til os via [support](/support) |
| `invalid_query_parameter` | 400 | Forkert parameter på en DAWA-sti, fx `srid` ud over 4326 og 25832 | Læs `details[].loc` og `msg` |
| `invalid_kvh`, `invalid_kvhx` | 400 | Nøglen har forkert længde | 12 tegn for kvh, 19 for kvhx |
| `invalid_cirkel`, `invalid_polygon` | 400 | Forkert geometri | Se [Fejl 400 og 404](/guides/fejl-400-og-404) |
| `area_too_large` | 400 | `cirkel` eller `polygon` dækker for mange adresser | Gør området mindre, eller tilføj `postnr` eller `kommunekode` |
| `validation_error` | 422 | Forkert input på `/api/v1` | Læs `error.details` |
| `not_found` | 404 | Id'et eller koden findes ikke | Tjek id-typen |
| `route_not_found` | 404 | Stien findes ikke | Tjek URL'en |
| `unknown_entity` | 404 | Ukendt replikeringsentitet | `details.supported` viser de gyldige |
| `quota_exceeded` | 429 | Gratis månedskvote (inkl. buffer) eller dit budgetloft er brugt | Vent til den 1., opgradér eller ret budgetloftet. Se `details.reason` |
| `rate_limit_per_minute` | 429 | For mange kald i minuttet | Vent `Retry-After` sekunder |
| `rate_limit_per_visitor` | 429 | En besøgende har kaldt for meget via browsernøgle | Vent `Retry-After` sekunder |
| `upstream_unavailable` | 503 | Et offentligt register svarer ikke | Prøv igen efter `Retry-After` |
| `upstream_busy` | 503 | Kortvarig spærring af kolde berigelser | Prøv igen efter `Retry-After` |
| `internal_error` | 500 | Fejl hos os | Prøv igen, og skriv til os via [support](/support), hvis den bliver ved |

## En hjælpefunktion til begge formater

Funktionen her giver dig `status`, `code`, `message`, `details` og `retry_after`, uanset om fejlen kom fra en DAWA-sti, fra `/api/v1` eller fra proxyen. Eksemplet slår et id op, der ikke findes:

```python id=read-error-python run=live expect="not_found"
import os
import requests


def read_error(resp: requests.Response) -> dict:
    """Normaliser et fejlsvar fra api.danadresse.dk."""
    info = {
        "status": resp.status_code,
        "code": None,
        "message": resp.reason,
        "details": None,
        "retry_after": resp.headers.get("Retry-After"),
    }
    try:
        body = resp.json()
    except ValueError:
        info["message"] = resp.text[:200] or resp.reason  # fx HTML fra en 502
        return info
    if not isinstance(body, dict):
        return info
    err = body.get("error")
    if isinstance(err, dict):  # /api/v1
        info.update(code=err.get("code"), message=err.get("message"),
                    details=err.get("details"))
    elif "type" in body or "title" in body:  # DAWA-stier (RFC 7807)
        info.update(code=body.get("code") or body.get("type"),
                    message=body.get("title"), details=body.get("details"))
    return info


r = requests.get(
    "https://api.danadresse.dk/adresser/00000000-0000-0000-0000-000000000000",
    headers={"X-Api-Key": os.environ["DANADRESSE_API_KEY"]},
    timeout=10,
)
if not r.ok:
    fejl = read_error(r)
    print(fejl["status"], fejl["code"], "-", fejl["message"])
```

Output:

```text id=read-error-output run=no
404 not_found - Adresse 00000000-0000-0000-0000-000000000000 not found
```

Samme funktion i JavaScript, til Node 18+ og browsere:

```js id=read-error-js run=live expect="not_found"
export async function readError(res) {
  const info = {
    status: res.status,
    code: null,
    message: res.statusText,
    details: null,
    retryAfter: res.headers.get("Retry-After"),
  };
  const text = await res.text();
  let body;
  try {
    body = JSON.parse(text);
  } catch {
    info.message = text.slice(0, 200) || res.statusText; // fx HTML fra en 502
    return info;
  }
  if (body && typeof body.error === "object" && body.error !== null) {
    info.code = body.error.code ?? null;
    info.message = body.error.message ?? info.message;
    info.details = body.error.details ?? null;
  } else if (body && ("type" in body || "title" in body)) {
    info.code = body.code ?? body.type ?? null;
    info.message = body.title ?? info.message;
    info.details = body.details ?? null;
  }
  return info;
}

const res = await fetch(
  "https://api.danadresse.dk/adresser/00000000-0000-0000-0000-000000000000",
  { headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY } },
);
if (!res.ok) {
  const fejl = await readError(res);
  console.log(fejl.status, fejl.code, "-", fejl.message);
}
```

`Retry-After` er en af de headere, API'et gør synlige via CORS, så `retryAfter` virker også i en browser. Se [Grænser, kvoter og 429](/guides/graenser-og-429).

## Byg logikken på code

Et eksempel på, hvordan koderne fordeler sig i en klient:

```python id=branch-on-code run=syntax
RETRY = {"rate_limit_per_minute", "rate_limit_per_visitor", "upstream_unavailable",
         "upstream_busy", "internal_error"}
FIX_CONFIG = {"missing_api_key", "invalid_api_key", "key_blocked", "origin_not_allowed",
              "browser_key_scope", "sandbox_scope", "ip_not_allowed", "account_suspended"}


def classify(err: dict) -> str:
    """Returner 'retry', 'config', 'stop' eller 'bug' for et normaliseret fejlsvar."""
    if err["code"] in RETRY or err["status"] in (502, 504):
        return "retry"
    if err["code"] in FIX_CONFIG:
        return "config"
    if err["code"] == "quota_exceeded":
        return "stop"
    return "bug"
```

- `retry`: prøv igen med ventetid. Se [Grænser, kvoter og 429](/guides/graenser-og-429).
- `config`: prøv ikke igen. Nøglen eller dens opsætning skal rettes.
- `stop`: stop kaldene indtil kvoten nulstilles.
- `bug`: forkerte parametre eller id'er i din kode. Log hele svaret.

## Næste skridt

- [Grænser, kvoter og 429](/guides/graenser-og-429)
- [Fejl 400 og 404](/guides/fejl-400-og-404)
- [Fejl 403: adgang nægtet](/guides/fejl-403)
