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.
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
| 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.
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
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":
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:
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 |
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. The wash only matches addresses that are in use, and aktueladresse in the result is the current version:
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 |:
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)