# Browsernøgler og tilladte domæner

> Lås en nøgle til dine domæner i dashboardet eller med PATCH /api/v1/keys/{id}, så den kan stå i en offentlig webside uden at blive misbrugt.
>
> https://danadresse.dk/guides/browser-noegler-domaener · verified 2026-11-02
Et adressefelt, der kalder API'et direkte fra browseren, skal have nøglen med i sidens kildekode. Alle kan åbne udviklerværktøjerne og kopiere den. En browsernøgle er en almindelig nøgle med en liste over tilladte domæner. API'et svarer kun, når kaldet kommer fra en side på et af dem.

Uden låsen ville nøglen også blive fanget af lækagekontrollen: en live-nøgle, der bruges fra mange forskellige IP-adresser på én dag, bliver markeret som lækket og spærret efter en frist. En domænelåst nøgle er undtaget fra den kontrol, fordi det er forventet, at hver besøgende har sin egen IP.

## 1. Opret en nøgle til browseren

Opret en ny nøgle under [Nøgler](/dashboard/keys) og giv den et navn, der siger, hvor den bruges, fx "webshop checkout". Brug aldrig den samme nøgle i browseren og på din server:

- En browsernøgle virker kun, når kaldet har en `Origin`- eller `Referer`-header fra et tilladt domæne. Kald fra en server har ingen af dem og bliver afvist med 403.
- En browsernøgle må kun lave adresseopslag (se trin 5).

Du kan også låse en testnøgle (`dawa_test_…`) til `localhost` og bruge den under udvikling.

## 2. Sæt domænerne i dashboardet

Under hver nøgle på [Nøgler](/dashboard/keys) er der en sektion, der hedder **Tilladte domæner**. Skriv domænerne i feltet, adskilt af komma, mellemrum eller linjeskift, og klik **Gem**. Et tomt felt gør nøglen til en almindelig servernøgle igen.

Du kan indsætte hele URL'er. `https://www.firma.dk/kassen` bliver gemt som `www.firma.dk`. Port og protokol bliver fjernet.

Når låsen afviser kald, viser nøglen, hvilke domæner der er afvist de sidste 7 dage og hvor mange gange, fx `shop.firma.dk (108×)`. Er det jeres egen side, så tilføj domænet til listen.

## 3. Eller sæt dem med API'et

Har du mange nøgler eller sites, kan du sætte listen med `PATCH /api/v1/keys/{id}`. Endpointet kræver en JWT, ikke en API-nøgle. Du får den fra `POST /api/v1/auth/login` med din e-mail og adgangskode. Logger du ind med Google eller kode på mail og har ingen adgangskode, så sæt en via [Glemt adgangskode](/dashboard/forgot) først.

Scriptet logger ind, viser dine nøgler og sætter domænerne på én af dem. Det kræver `pip install requests` og tre miljøvariabler:

```python id=patch-origins run=syntax
import os
import sys

import requests

BASE = "https://api.danadresse.dk"

login = requests.post(
    f"{BASE}/api/v1/auth/login",
    json={"email": os.environ["DANADRESSE_EMAIL"], "password": os.environ["DANADRESSE_PASSWORD"]},
    timeout=10,
)
login.raise_for_status()
auth = {"Authorization": f"Bearer {login.json()['access_token']}"}

# Vis nøglerne, så du kan finde id'et
for k in requests.get(f"{BASE}/api/v1/keys", headers=auth, timeout=10).json():
    print(k["id"], k["mode"], k["name"], k["allowed_origins"])

key_id = os.environ["DANADRESSE_KEY_ID"]
r = requests.patch(
    f"{BASE}/api/v1/keys/{key_id}",
    headers=auth,
    json={"allowed_origins": ["firma.dk", "www.firma.dk", "*.firma.dk", "localhost"]},
    timeout=10,
)
if r.status_code != 200:
    fejl = r.json().get("error", {})
    sys.exit(f"{r.status_code} {fejl.get('code')}: {fejl.get('message')}")
print("Tilladte domæner:", r.json()["allowed_origins"])
```

Svaret indeholder listen, som den blev gemt efter normalisering:

```json id=patch-response run=no
{
  "id": "4f1c0b7e-2a9d-4c55-9a51-0d3c7f1e8b20",
  "allowed_origins": ["firma.dk", "www.firma.dk", "*.firma.dk", "localhost"]
}
```

`{"allowed_origins": []}` fjerner låsen. Et body uden `allowed_ips` ændrer ikke en eventuel IP-liste på nøglen. Ugyldige domæner giver 422 med koden `invalid_origins` og en dansk fejltekst. En nøgle, der ikke findes, er tilbagekaldt eller tilhører en anden konto, giver 404.

## 4. Sådan matches domænerne

API'et tager værtsnavnet fra `Origin`-headeren. Mangler den, bruges `Referer`. Protokol og port tæller ikke med.

| Mønster | Matcher | Matcher ikke |
|---|---|---|
| `firma.dk` | `firma.dk` | `www.firma.dk`, `shop.firma.dk` |
| `www.firma.dk` | `www.firma.dk` | `firma.dk` |
| `*.firma.dk` | `www.firma.dk`, `shop.firma.dk`, `a.b.firma.dk` | `firma.dk` |
| `localhost` | `localhost` på alle porte | `127.0.0.1` |
| `127.0.0.1` | `127.0.0.1` på alle porte | `localhost` |

Regler for listen:

- Et wildcard må kun stå forrest, som `*.firma.dk`. `*`, `www.*` og `*.*.firma.dk` afvises.
- `*.firma.dk` dækker kun underdomæner. Kører dit site både på `firma.dk` og `www.firma.dk`, skal du skrive begge eller `firma.dk` og `*.firma.dk`.
- Højst 40 domæner pr. nøgle og højst 700 tegn i alt. Brug et wildcard i stedet for mange underdomæner.
- `localhost` er tilladt til udvikling. Åbner du en HTML-fil direkte fra disken (`file://`), sender browseren `Origin: null`, og kaldet bliver afvist. Start en lokal server i stedet, fx `npx serve` eller `python3 -m http.server`.

## 5. Hvad en browsernøgle ikke må

En domænelåst nøgle dækker adresseopslag: `/autocomplete`, `/adresser`, `/adgangsadresser`, `/datavask/adresser`, `/postnumre`, `/kommuner` og de øvrige DAWA-kompatible endpoints. Den bliver afvist med 403 og koden `browser_key_scope` på:

- `/api/v1/enrich/…` (BBR, DAGI, ejendom, klima osv.)
- `/api/v1/datavask/parse`
- `/api/v1/stats/national`

De kalder videre til offentlige registre og skal kaldes fra din server med en servernøgle. Har du brug for dem på en webside, så lav en proxy som i [Next.js route handler som proxy](/guides/nextjs-proxy).

## 6. Grænser for en browsernøgle

Kald med en browsernøgle tæller i kontoens månedlige kvote og minutgrænse, ligesom alle andre kald. Derudover har hver besøgendes IP-adresse sin egen grænse på 120 kald i minuttet pr. nøgle. Overskrides den, svarer API'et 429 med koden `rate_limit_per_visitor` og en `Retry-After`-header med antal sekunder, til minutvinduet starter forfra (1-60). På den måde kan én besøgende ikke bruge hele din kvote. Afviste kald tæller ikke i din kvote.

Svar fra API'et, også fejl, har CORS-headere og gør `Retry-After`, `X-RateLimit-*`, `X-RateLimit-Minute-*` og `X-Quota-*` læsbare for JavaScript. Efter en 429 læser du `res.headers.get("Retry-After")` og venter så længe før næste kald. Med debounce på 150-200 ms i dit adressefelt kommer en almindelig bruger ikke i nærheden af grænsen.

## 7. Test låsen

Fra terminalen kan du sætte `Origin`-headeren selv og se, om låsen opfører sig, som du forventer. Byt nøglen ud med din browsernøgle:

```bash id=test-origin run=no
# Din browser-nøgle
export BROWSER_KEY="…"

# Tilladt domæne → 200 og en liste af forslag
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "X-Api-Key: $BROWSER_KEY" -H "Origin: https://www.firma.dk" \
  "https://api.danadresse.dk/autocomplete?q=Strandvejen"

# Andet domæne → 403 origin_not_allowed
curl -s -H "X-Api-Key: $BROWSER_KEY" -H "Origin: https://andet.dk" \
  "https://api.danadresse.dk/autocomplete?q=Strandvejen"
```

Det andet kald giver et svar som dette. Teksten fortæller, hvilke domæner nøglen er låst til, og hvilket domæne kaldet kom fra:

```json id=origin-not-allowed run=no
{
  "type": "ForbiddenError",
  "title": "This API key is locked to firma.dk, www.firma.dk and this request came from andet.dk. Add the domain under API-nøgler → Tilladte domæner, or use a server-side key.",
  "details": null,
  "code": "origin_not_allowed"
}
```

## Hvor meget beskytter låsen?

`Origin` og `Referer` sættes af browseren, og et webscript på en anden side kan ikke ændre dem. Uden for en browser kan alle sætte de headers, de vil, som curl-eksemplet viser. Låsen forhindrer altså, at andre kopierer din nøgle ind på deres egen hjemmeside. Den forhindrer ikke et script på en server i at efterligne dit domæne. Grænsen pr. besøgende og kontoens kvote er bagstopperen.

Tror du, at nøglen bliver misbrugt, så opret en ny, sæt de samme domæner på, skift den ud på siden og tilbagekald den gamle. Se [Roter nøgler](/guides/roter-noegler).

## Næste skridt

- [Autocomplete-widget i ren JavaScript](/guides/vanilla-autocomplete-widget)
- [Next.js route handler som proxy](/guides/nextjs-proxy) — når nøglen slet ikke skal i browseren.
- [Fejl 403](/guides/fejl-403) — alle 403-koder og hvad de betyder.
