Browser keys and allowed domains
Lock a key to your domains in the dashboard or with PATCH /api/v1/keys/{id}, so it can sit in a public web page without being abused.
An address field that calls the API straight from the browser has to carry the key in the page source. Anyone can open the developer tools and copy it. A browser key is a regular key with a list of allowed domains. The API answers only when the call comes from a page on one of them.
Without the lock the key would also be caught by leak control: a live key used from many different IP addresses in one day is flagged as leaked and blocked after a grace period. A domain-locked key is exempt from that check, because every visitor is expected to have their own IP.
1. Create a key for the browser
Create a new key under Keys and give it a name that says where it is used, for example "webshop checkout". Never use the same key in the browser and on your server:
- A browser key only works when the call carries an
OriginorRefererheader from an allowed domain. Calls from a server have neither and are refused with 403. - A browser key may only do address lookups (see step 5).
You can also lock a test key (dawa_test_…) to localhost and use it during development.
2. Set the domains in the dashboard
Under each key on Keys there is a section called Tilladte domæner (allowed domains). Type the domains in the field, separated by commas, spaces or line breaks, and click Gem (save). An empty field turns the key back into a regular server key.
You can paste full URLs. https://www.firma.dk/kassen is stored as www.firma.dk. Port and scheme are dropped.
When the lock turns calls away, the key shows which domains were refused in the last 7 days and how many times, for example shop.firma.dk (108×). If it is your own site, add the domain to the list.
3. Or set them with the API
With many keys or sites you can set the list with PATCH /api/v1/keys/{id}. The endpoint needs a JWT, not an API key. You get one from POST /api/v1/auth/login with your email and password. If you sign in with Google or an emailed code and have no password, set one through Forgot password first.
The script signs in, lists your keys and sets the domains on one of them. It needs pip install requests and three environment variables:
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']}"} # List the keys so you can find the id 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: err = r.json().get("error", {}) sys.exit(f"{r.status_code} {err.get('code')}: {err.get('message')}") print("Allowed domains:", r.json()["allowed_origins"])
The response holds the list as stored after normalisation:
{
"id": "4f1c0b7e-2a9d-4c55-9a51-0d3c7f1e8b20",
"allowed_origins": ["firma.dk", "www.firma.dk", "*.firma.dk", "localhost"]
}{"allowed_origins": []} removes the lock. A body without allowed_ips leaves any IP list on the key untouched. Invalid domains give 422 with the code invalid_origins and an error text in Danish. A key that does not exist, is revoked or belongs to another account gives 404.
4. How domains are matched
The API takes the host name from the Origin header. If it is missing, Referer is used. Scheme and port do not count.
| Pattern | Matches | Does not match |
|---|---|---|
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 on any port | 127.0.0.1 |
127.0.0.1 | 127.0.0.1 on any port | localhost |
Rules for the list:
- A wildcard may only come first, as in
*.firma.dk.*,www.*and*.*.firma.dkare refused. *.firma.dkcovers subdomains only. If your site runs on bothfirma.dkandwww.firma.dk, list both, orfirma.dkplus*.firma.dk.- At most 40 domains per key and at most 700 characters in total. Use a wildcard instead of many subdomains.
localhostis allowed for development. If you open an HTML file straight from disk (file://), the browser sendsOrigin: nulland the call is refused. Start a local server instead, for examplenpx serveorpython3 -m http.server.
5. What a browser key may not do
A domain-locked key covers address lookups: /autocomplete, /adresser, /adgangsadresser, /datavask/adresser, /postnumre, /kommuner and the other DAWA-compatible endpoints. It is refused with 403 and the code browser_key_scope on:
/api/v1/enrich/…(BBR, DAGI, property, climate etc.)/api/v1/datavask/parse/api/v1/stats/national
These call on to public registers and must be called from your server with a server key. If you need them on a web page, build a proxy as in Next.js route handler as a proxy.
6. Limits for a browser key
Calls with a browser key count against the account's monthly quota and per-minute limit, like any other call. On top of that, each visitor IP has its own cap of 120 calls per minute per key. Above it the API answers 429 with the code rate_limit_per_visitor and a Retry-After header with the seconds until the minute window resets (1-60). That way one visitor cannot use up your whole quota. Refused calls are not counted in your quota.
Responses from the API, errors included, carry CORS headers and expose Retry-After, X-RateLimit-*, X-RateLimit-Minute-* and X-Quota-* to JavaScript. After a 429, read res.headers.get("Retry-After") and wait that long before the next call. With a debounce of 150-200 ms in your address field a normal user never gets near the cap.
7. Test the lock
From a terminal you can set the Origin header yourself and check that the lock behaves as you expect. Swap in your browser key:
# Your browser key export BROWSER_KEY="…" # Allowed domain → 200 and a list of suggestions 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" # Other domain → 403 origin_not_allowed curl -s -H "X-Api-Key: $BROWSER_KEY" -H "Origin: https://andet.dk" \ "https://api.danadresse.dk/autocomplete?q=Strandvejen"
The second call returns something like this. The text says which domains the key is locked to and which domain the call came from:
{
"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"
}How much does the lock protect?
Origin and Referer are set by the browser, and a script on another site cannot change them. Outside a browser anyone can send whatever headers they like, as the curl example shows. The lock therefore stops other people from pasting your key into their own website. It does not stop a script on a server from pretending to be your domain. The per-visitor cap and the account quota are the backstop.
If you think the key is being abused, create a new one, add the same domains, swap it in on the page and revoke the old one. See Rotate keys.
Next steps
- Address autocomplete widget in plain JavaScript
- Next.js route handler as a proxy — when the key must not be in the browser at all.
- Error 403 — every 403 code and what it means.
Related guides
Markdown source Spotted an error? Tell us API-reference (Swagger)