# Store the address id and re-validate later

> Why you should store the address id and the access-address id, how to re-validate stored addresses in batches, and what to do when an address is retired.
>
> https://danadresse.dk/en/guides/gem-adresse-id · verified 2026-11-02
Once a customer has picked an address in autocomplete, or datavask has accepted it, you hold two UUIDs: the address id and the access-address id. Store them. Text changes, ids do not.

## Why the id and not just the text?

- **Street names change.** When a street is renamed or an area gets a new postcode, the address keeps its id. With the id you can fetch the new spelling. With only the text you have an address that no longer exists as written.
- **Lookups are exact.** `GET /adresser/{id}` gives exactly one address. A text search can give none, one or several.
- **Enrichment needs an id.** BBR, cadastre, administrative areas and distance are looked up by id (see [Get BBR data](/en/guides/bbr-bygningsdata)).
- **Duplicates.** Two customers at the same address have the same id, even if they spelled it differently.

## The two ids

| Id | Points to | Where you get it |
|---|---|---|
| Address id | The specific dwelling or unit, with floor and door | `data.id` in autocomplete (type `adresse`), `adresse.id` in datavask, `id` in `/adresser` |
| Access-address id | The building entrance, without floor and door | `data.adgangsadresseid` in autocomplete, `adresse.adgangsadresseid` in datavask, `adgangsadresse.id` in `/adresser/{id}` |

Store both. The address id is the one you validate on. The access-address id is the building: coordinates, BBR and cadastre belong to it, and it stays valid if the customer moves from 2. th to 3. tv on the same staircase. `/api/v1/enrich/*` accepts either id, and `/adgangsadresser/{id}` with an address id answers with the access address the address belongs to. See [Address or access address](/en/guides/adresse-vs-adgangsadresse).

A suggestion at building level (type `adgangsadresse` in autocomplete, or a result from `/datavask/adgangsadresser`) only has the access-address id, and it is in `data.id` or `adresse.id` respectively. Leave the address id empty in that case.

## A table that lasts

```sql id=schema-sql run=no
CREATE TABLE kunde_adresse (
  kunde_id            BIGINT       NOT NULL,
  adresse_id          CHAR(36)     NULL,      -- with floor/door, can be missing
  adgangsadresse_id   CHAR(36)     NULL,
  adressetekst        VARCHAR(200) NOT NULL,  -- as shown to the customer, for invoices and letters
  postnr              CHAR(4)      NOT NULL,  -- text, never a number
  kommunekode         CHAR(4)      NULL,
  status              TINYINT      NULL,      -- 1 current, 3 provisional, 2 retired, 4 shelved
  valideret           TIMESTAMP    NULL,
  PRIMARY KEY (kunde_id),
  KEY (adresse_id),
  KEY (adgangsadresse_id)
);
```

Store the text next to the id. An invoice has to show the address as it was when the invoice was made, and you need something to show if the API does not answer.

## Re-validate stored addresses

Run a job, for example once a week or before a mailing. `GET /adresser` takes up to about 100 ids at a time separated by `|`. Add `medtagnedlagte`, otherwise retired addresses simply drop out of the response and you cannot tell "retired" from "unknown":

```python id=revalider-python run=live expect="ukendt"
import os
import requests

s = requests.Session()
s.headers["X-Api-Key"] = os.environ["DANADRESSE_API_KEY"]

gemte = {
    "fec4202d-1015-425d-a3f5-5d21d03194b6": "Strandvejen 100, 2900 Hellerup",
    "0a3f50c3-d930-32b8-e044-0003ba298018": "Paludan-Müllers Vej 22, 8000 Aarhus C",
    "00000000-0000-4000-8000-000000000000": "Tastefejl 1, 9999 Ingensteds",
}

def bunker(ids, n=100):
    ids = list(ids)
    for i in range(0, len(ids), n):
        yield ids[i:i + n]

for bunke in bunker(gemte):
    r = s.get("https://api.danadresse.dk/adresser", timeout=30, params={
        "id": "|".join(bunke), "medtagnedlagte": "", "struktur": "mini", "per_side": len(bunke)})
    r.raise_for_status()
    fundet = {a["id"]: a for a in r.json()}
    for adresse_id in bunke:
        a = fundet.get(adresse_id)
        if a is None:
            print(f"ukendt   {adresse_id}  ({gemte[adresse_id]})")
        elif a["status"] in (2, 4):
            print(f"nedlagt  {adresse_id}  {a['betegnelse']}")
        elif a["betegnelse"] != gemte[adresse_id]:
            print(f"ændret   {adresse_id}  {gemte[adresse_id]} -> {a['betegnelse']}")
        else:
            print(f"ok       {adresse_id}  {a['betegnelse']}")
```

The output uses the Danish words `ukendt` (unknown), `nedlagt` (retired) and `ændret` (changed). The response does not come back in the order of the ids, so match on `id`.

For single lookups, e.g. when a customer logs in, `GET /adresser/{id}` is simplest. It returns the address whatever its status, and an unknown id gives `404`:

```js id=revalider-node run=live expect="status"
async function tjekAdresse(id) {
  const res = await fetch(`https://api.danadresse.dk/adresser/${id}`, {
    headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY },
    signal: AbortSignal.timeout(5000),
  });
  if (res.status === 404) return { tilstand: "unknown" };
  if (!res.ok) return { tilstand: "could not check", http: res.status };
  const a = await res.json();
  const aktiv = a.status === 1 || a.status === 3;
  return { tilstand: aktiv ? "ok" : "retired", status: a.status, tekst: a.adressebetegnelse };
}

console.log(JSON.stringify(await tjekAdresse("fec4202d-1015-425d-a3f5-5d21d03194b6")));
```

## What do you do with the result?

| Result | Action |
|---|---|
| ok | Update `valideret` |
| changed text | Update `adressetekst`, `postnr` and `kommunekode` to the new values. The id is the same |
| retired (status 2 or 4) | Do not ship goods there. Find the replacement as described below, or ask the customer for a new address |
| unknown (missing from the response, or `404`) | The id is wrong or comes from another source. Wash the text again with [datavask](/en/guides/datavask-i-checkout) |
| `429` or `503` | Retry later. Do not touch the stored data |

A status 3 (provisional) is an address that has been created but is not in use yet. It is valid to store.

## Retired: find the replacement

An address is retired when it is deleted or replaced, for example when two flats are merged or a house gets a new house number. The old id does not come back. Wash the stored text with [datavask](/en/guides/datavask-i-checkout). The wash only matches addresses that are in use, and `aktueladresse` in the result is the current version:

```python id=afloeser-python run=live expect="kategori="
import os
import requests

def find_afloeser(adressetekst):
    r = requests.get(
        "https://api.danadresse.dk/datavask/adresser",
        params={"betegnelse": adressetekst},
        headers={"X-Api-Key": os.environ["DANADRESSE_API_KEY"]},
        timeout=10,
    )
    r.raise_for_status()
    svar = r.json()
    if svar["kategori"] in ("A", "B") and svar["resultater"]:
        return svar["kategori"], [svar["resultater"][0]["aktueladresse"]]
    return svar["kategori"], [x["aktueladresse"] for x in svar["resultater"][:5]]

# An address that was retired in October 2026
kategori, kandidater = find_afloeser("Bobakken 27, st., 4652 Hårlev")
print(f"kategori={kategori}")
for a in kandidater:
    print(a["id"], a["adgangsadresseid"], a["vejnavn"], a["husnr"], a["etage"], a["dør"], a["status"])
```

If the category is A or B, update `adresse_id`, `adgangsadresse_id` and the text to the new values. If it is C, show the candidates to the customer at the next contact and let them choose. Here the ground floor was retired, and the wash suggests the address at entrance level.

## See what happened

`GET /historik/adresser?id=…` (and `/historik/adgangsadresser?id=…`) returns the address's versions with `status`, `virkningstart` and `virkningslut`, oldest first. It takes several ids separated by `|`:

```bash id=historik-curl run=live expect="virkningstart"
curl -s "https://api.danadresse.dk/historik/adresser?id=c06a95ac-74d6-4f76-85c6-992d8ad896ed" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"
```

The history covers changes from July 2026, when our change log started. An address that has not changed since then has one version. See [Retired addresses and address history](/en/guides/nedlagte-adresser-og-historik).

## Stay up to date

If you want to know as soon as a stored address changes, instead of checking at fixed intervals, use [webhooks](/en/guides/webhooks-opsaetning) or [events](/en/guides/replikering-haendelser).

## Next steps

- [Retired addresses and address history](/en/guides/nedlagte-adresser-og-historik)
- [Address or access address: the data model](/en/guides/adresse-vs-adgangsadresse)
- [Validate the address at checkout with datavask](/en/guides/datavask-i-checkout)
