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.

4 min Level: Beginner ✓ Code tested

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.

With curl:

Shell#header-curl
curl -s "https://api.danadresse.dk/postnumre/8000" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"

With Python and requests:

Python#header-python
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:

JavaScript#header-node
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#header-php
<?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
Shell#query-curl
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.

Which method should I use?

SituationMethodKey type
Backend, cron job, ETL, CIX-Api-Key headerServer key from an environment variable
fetch in the browser, React, VueHeader or api_keyDomain-locked browser key
<script src> for a widget?key= / api_key in the URLDomain-locked browser key
Google Sheets IMPORTDATA, simple toolsapi_key in the URLA separate key you can rotate
Development and automated testsHeaderTest 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#redact-filter
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, 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 for what happens if it is not replaced.

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 →