# Rotate API keys without downtime

> Replace an API key without a single call failing, respond to a leaked or blocked key, and store the key safely on your platform.
>
> https://danadresse.dk/en/guides/roter-noegler · verified 2026-11-02
An API key should be replaced when an employee with access leaves, when the key has been visible somewhere it should not (a commit, a log, a screen share), or when we have flagged it as possibly leaked. Replacing it at fixed intervals is also a good habit.

Because an account can have several active keys at once, you can switch without downtime: create the new one, put it to use, and revoke the old one only when nothing uses it any more.

## 1. Create a new key

Go to [Keys](/dashboard/keys), give the new key a name that says where it will be used (e.g. `webshop-prod-2026-10`), choose *live* and click **Opret** (create). The key is shown only once. We store only a hash of it and cannot show it again.

The number of active live keys per account is limited:

| Plan | Live keys |
|---|---|
| Hobby | 2 |
| Basis | 8 |
| Pro | 20 |
| Enterprise | 50 |

Test keys do not count. If you are at the cap you cannot create a new live key (`403 key_cap_reached` from the API). Revoke a key you do not use before you start. If you only have one key in use, there is always room for one more, also on Hobby.

If the old key was a browser key with **Tilladte domæner** (allowed domains), enter the same domains on the new one before you use it. Otherwise it will not answer from your pages, or it will be seen from many IP addresses and flagged as leaked.

## 2. Test the new key

Make one call with the new key before it goes to production:

```bash id=test-new-key run=live expect="200"
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://api.danadresse.dk/postnumre/8000" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"
```

`200` means the key works. `401` means it was copied wrong, often with an extra space or line break.

## 3. Put the new key to use

Put the new key in your secret store and restart or redeploy your service. Both keys work at the same time, so it does not matter that some instances run with the old one for a while.

It gets easier next time if the code reads the key from a file or an environment variable at start-up instead of having it in the code:

```python id=load-key run=syntax
import os
from pathlib import Path


def api_key() -> str:
    """Read the key from a secret file (Docker/Kubernetes) or from an environment variable."""
    path = os.environ.get("DANADRESSE_API_KEY_FILE")
    if path:
        return Path(path).read_text(encoding="utf-8").strip()
    key = os.environ.get("DANADRESSE_API_KEY", "").strip()
    if not key:
        raise RuntimeError("DANADRESSE_API_KEY or DANADRESSE_API_KEY_FILE is missing")
    return key
```

`.strip()` removes the line break that many tools put at the end of a secret file.

Where the key belongs depends on the platform:

| Platform | Place |
|---|---|
| Local development | A `.env` file listed in `.gitignore` |
| Docker / Kubernetes | Docker secrets or a Kubernetes `Secret`, mounted as a file or environment variable |
| GitHub Actions | Repository or environment secrets |
| Vercel, Netlify and similar | The project's environment variables, marked as secret |
| AWS, Azure, Google Cloud | Secrets Manager or Parameter Store, Key Vault, Secret Manager |
| PHP on shared hosting | A configuration file outside the web root, or an environment variable in the virtual host setup |
| WordPress | A constant in `wp-config.php` that is not in your repository |
| Browser | Nothing can be kept secret. Use a domain-locked browser key |

## 4. Wait until the old key is quiet

The **Sidste brug** (last used) column on [Keys](/dashboard/keys) shows when each key was last used. It updates at most once a minute. Wait until the time for the old key stops moving. If you have nightly jobs or cron tasks, wait a day so they have also run with the new key.

If the old key keeps being used, something still runs with it. Typical places are a forgotten staging server, a cron job, a colleague's local `.env` or a plugin setting in a CMS.

## 5. Revoke the old key

Click **Tilbagekald** (revoke) next to the old key. It takes effect at once: the next call with it gets `401 invalid_api_key`. If you missed a place in step 4, that place will start failing now, so watch your errors for the next few hours.

## When a key is leaked or blocked

We watch how many different IP addresses a live key is used from in a day. If it is many, it looks like a key that got out:

1. The key is flagged, responses get the header `X-Danadresse-Key-Status: warn`, and you get an email.
2. If it keeps being used from many IP addresses after 72 hours, it is blocked. Calls get `403 key_blocked`, and the header says `blocked`.

Test keys and domain-locked browser keys are not covered, because they are meant to be used from many places.

What to do depends on the cause:

- **The key sits in a web page.** The many IP addresses are your visitors, so the key has not leaked. Lock it to your domains under **Tilladte domæner**. See [Browser keys and allowed domains](/en/guides/browser-noegler-domaener).
- **The key got out** (a public repository, an app, a customer). Follow steps 1 to 5 right away, and revoke the old key without waiting for step 4 if you know it is being misused.
- **You know why the number is high** (e.g. a server behind many changing outbound IP addresses). Use **Lås op** (lift the block) or **Nulstil advarsel** (reset warning) on the key in the dashboard. On Enterprise you can instead lock the key to your IP addresses.

Also check whether the key is in your Git history. A key deleted in the latest commit is still in the older ones:

```bash id=search-history run=syntax
git log -p --all -S "dawa_live_" -- . | grep -n "dawa_live_" | head
```

If you find it, rotate the key. Rewriting the history is not enough, because copies of the repository may exist elsewhere.

## Next steps

- Lock a key to domains: [Browser keys and allowed domains](/en/guides/browser-noegler-domaener).
- See the key status in your logs: [Monitor usage](/en/guides/overvaag-forbrug).
- The full list before production: [Go-live checklist](/en/guides/go-live-tjekliste).
