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.

5 min Level: Beginner ✓ Code tested

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).
  • Duplicates. Two customers at the same address have the same id, even if they spelled it differently.

The two ids

IdPoints toWhere you get it
Address idThe specific dwelling or unit, with floor and doordata.id in autocomplete (type adresse), adresse.id in datavask, id in /adresser
Access-address idThe building entrance, without floor and doordata.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.

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#schema-sql
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#revalider-python
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:

JavaScript#revalider-node
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?

ResultAction
okUpdate valideret
changed textUpdate 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
429 or 503Retry 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. The wash only matches addresses that are in use, and aktueladresse in the result is the current version:

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

Shell#historik-curl
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.

Stay up to date

If you want to know as soon as a stored address changes, instead of checking at fixed intervals, use webhooks or events.

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 →