# The API key in a header or in the URL (api_key)

> Choose between the X-Api-Key header and ?api_key= in the URL, and send the key correctly from curl, Python, Node and PHP.
>
> https://danadresse.dk/en/guides/api-noegle-header-eller-query · verified 2026-11-02
Every call to `https://api.danadresse.dk` needs an API key. You can send it in two ways: in the `X-Api-Key` header or as the `api_key` query parameter in the URL. Both work on every endpoint, on the DAWA-compatible paths and on `/api/v1`.

Use the header when you can. This guide explains why, and when the URL form is the right choice.

## How the API reads the key

The server looks for the `X-Api-Key` header first. Only if it is missing or empty does it read `api_key` from the URL. Two consequences:

- If you send both, the header wins. A wrong key in the header gives `401 invalid_api_key`, even if the URL holds the right one.
- An empty header (for example `X-Api-Key: ` because the environment variable is not set) counts as missing, so `api_key` from the URL is used if present. Otherwise you get `401 missing_api_key`.

The header name is case-insensitive, so `x-api-key` and `X-API-KEY` work too. Whitespace around the key in the header is stripped.

## Header: recommended from servers and scripts

With curl:

```bash id=header-curl run=live expect="Aarhus"
curl -s "https://api.danadresse.dk/postnumre/8000" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"
```

With Python and `requests`:

```python id=header-python run=live expect="Aarhus"
import os
import requests

r = requests.get(
    "https://api.danadresse.dk/postnumre/8000",
    headers={"X-Api-Key": os.environ["DANADRESSE_API_KEY"]},
    timeout=10,
)
r.raise_for_status()
print(r.json()["navn"])
```

With Node 18 or newer:

```js id=header-node run=live expect="Aarhus"
const res = await fetch("https://api.danadresse.dk/postnumre/8000", {
  headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const postnummer = await res.json();
console.log(postnummer.navn);
```

With PHP and the curl extension:

```php id=header-php run=live expect="Aarhus"
<?php
$ch = curl_init("https://api.danadresse.dk/postnumre/8000");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => ["X-Api-Key: " . getenv("DANADRESSE_API_KEY")],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
    fwrite(STDERR, "HTTP $status: $body\n");
    exit(1);
}
echo json_decode($body, true)["navn"], "\n";
```

## Query parameter: when you cannot set a header

`?api_key=` exists because some places only let you give a URL:

- a `<script src="…">` tag, for example DAWA's old widget: `https://api.danadresse.dk/js/autocomplete/dawa-autocomplete2.min.js?key=dawa_live_…` (the widget then sends the key as `api_key` on every call)
- spreadsheets and tools that can only fetch a URL, such as `IMPORTDATA` in Google Sheets
- a quick test in the browser's address bar

```bash id=query-curl run=live expect="Aarhus"
curl -s "https://api.danadresse.dk/postnumre/8000?api_key=$DANADRESSE_API_KEY"
```

In the browser there is one more difference. A custom header such as `X-Api-Key` makes the browser send a CORS preflight (`OPTIONS`) before the real call. The API approves it and asks the browser to remember the answer for up to a day (some browsers keep it for less). The preflight does not count as an API call. With `api_key` in the URL the call is a "simple request" without a preflight. Both work, so pick whatever your library makes easiest.

## Why the header is the safer choice

A URL gets copied to many places you do not control:

- access logs in proxies, load balancers and firewalls between you and the API
- error-reporting tools that store the full request URL
- shell history, CI logs and screenshots of the browser's network tab
- HAR files you send to support

Our own application logs mask the value of `api_key=` (it becomes `api_key=***`, and the same goes for the `X-Api-Key` header). You cannot count on every other hop doing the same.

A header ends up in logs less often. It is not secret from whoever runs the code. In a browser, any visitor can see the key in the network tab, whether it sits in a header or in the URL. For code that runs on your visitors' machines, use a domain-locked browser key, see [Browser keys and allowed domains](/en/guides/browser-noegler-domaener).

## Which method should I use?

| Situation | Method | Key type |
|---|---|---|
| Backend, cron job, ETL, CI | `X-Api-Key` header | Server key from an environment variable |
| `fetch` in the browser, React, Vue | Header or `api_key` | Domain-locked browser key |
| `<script src>` for a widget | `?key=` / `api_key` in the URL | Domain-locked browser key |
| Google Sheets `IMPORTDATA`, simple tools | `api_key` in the URL | A separate key you can rotate |
| Development and automated tests | Header | Test key (`dawa_test_…`) |

## Mask the key in your own logs

If your application logs outgoing URLs, remove the key before the line is written. A Python logging filter that does the same as ours:

```python id=redact-filter run=syntax
import logging
import re

_SECRET = re.compile(r"(?i)((?:api_key|apikey)=|x-api-key\s*[:=]\s*)([^&\s\"']+)")


class RedactApiKey(logging.Filter):
    """Replace API keys in log messages with ***."""

    def filter(self, record: logging.LogRecord) -> bool:
        record.msg = _SECRET.sub(r"\1***", str(record.msg))
        if record.args:
            record.args = tuple(
                _SECRET.sub(r"\1***", a) if isinstance(a, str) else a
                for a in record.args
            )
        return True


handler = logging.StreamHandler()
handler.addFilter(RedactApiKey())
logging.basicConfig(level=logging.INFO, handlers=[handler])
logging.getLogger("demo").info("GET https://api.danadresse.dk/postnumre?api_key=dawa_live_secret")
```

The filter writes `api_key=***` instead of the key.

## If the key has leaked

If a key has been in a URL that ended up in a public log or a repository, create a new key under [Keys](/dashboard/keys), switch your configuration to it and delete the old one. A live key used from many different IP addresses on the same day is flagged as leaked and you get an email. See [Error 403](/en/guides/fejl-403) for what happens if it is not replaced.

## Next steps

- [Test keys and the sandbox](/en/guides/testnoegler-sandbox) for development and CI.
- [Browser keys and allowed domains](/en/guides/browser-noegler-domaener) before you put a key in frontend code.
- [Error 401: missing or invalid key](/en/guides/fejl-401) if the call is rejected.
