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.

6 min Level: Beginner ✓ Code tested

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 Origin or Referer header 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:

Python#patch-origins
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:

JSON#patch-response
{
  "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.

PatternMatchesDoes not match
firma.dkfirma.dkwww.firma.dk, shop.firma.dk
www.firma.dkwww.firma.dkfirma.dk
*.firma.dkwww.firma.dk, shop.firma.dk, a.b.firma.dkfirma.dk
localhostlocalhost on any port127.0.0.1
127.0.0.1127.0.0.1 on any portlocalhost

Rules for the list:

  • A wildcard may only come first, as in *.firma.dk. *, www.* and *.*.firma.dk are refused.
  • *.firma.dk covers subdomains only. If your site runs on both firma.dk and www.firma.dk, list both, or firma.dk plus *.firma.dk.
  • At most 40 domains per key and at most 700 characters in total. Use a wildcard instead of many subdomains.
  • localhost is allowed for development. If you open an HTML file straight from disk (file://), the browser sends Origin: null and the call is refused. Start a local server instead, for example npx serve or python3 -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:

Shell#test-origin
# 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:

JSON#origin-not-allowed
{
  "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

Related guides

Markdown source Spotted an error? Tell us API-reference (Swagger)

Try it with a free key

2,000 calls/month on the free plan. No credit card.

Create account →