Validate the address at checkout with datavask
Check the customer's address when the form is submitted, accept safe matches, confirm likely ones and ask for a fix on uncertain ones.
A wrong delivery address costs a returned parcel, a second shipment and an annoyed customer. Autocomplete catches most mistakes while the customer types, but not all of them: some paste the address from somewhere else, some have JavaScript turned off, and some orders arrive through an API. Server-side datavask (address washing) is the safety net that runs when the form is submitted.
The endpoint is GET /datavask/adresser (with floor and door) or GET /datavask/adgangsadresser (the building entrance only). The response has the same shape as DAWA's datavask. The top-level field kategori is A for an exact match, B for one safe match with small spelling differences and C for uncertain.
1. Send the address
The simplest is the whole address as one string in betegnelse:
curl -s -G "https://api.danadresse.dk/datavask/adresser" \ --data-urlencode "betegnelse=Strandvejen 100, 1., 2900 Hellerup" \ -H "X-Api-Key: $DANADRESSE_API_KEY"
The parser reads floor and door (2. tv, st. th, 1.mf), lower-case house letters (12a), a postcode anywhere in the text and aa/å spellings (Aabenraa, Kobenhavn). Upper case and a trailing full stop (1., TV) do not matter.
If your form has separate fields, you can send them instead: vejnavn, husnr, etage, dør, postnr and supplerendebynavn. The result is the same.
POST works too: POST /datavask/adresser with one address as a JSON body, either {"betegnelse": "…"} or the separate fields. Each request washes one address. To wash many addresses, see Clean a CSV export from your CRM.
2. Read the response
The customer typed "Strandvej" instead of "Strandvejen". The wash finds exactly one address and returns B:
{
"kategori": "B",
"resultater": [
{
"adresse": {
"id": "0a3f50a4-0c39-32b8-e044-0003ba298018",
"vejnavn": "Strandvejen",
"adresseringsvejnavn": "Strandvejen",
"husnr": "100",
"supplerendebynavn": null,
"postnr": "2900",
"postnrnavn": "Hellerup",
"status": 1,
"virkningstart": "2000-02-05T19:44:20.000Z",
"virkningslut": null,
"adgangsadresseid": "0a3f507c-0624-32b8-e044-0003ba298018",
"etage": "1",
"dør": null,
"href": "https://api.danadresse.dk/adresser/0a3f50a4-0c39-32b8-e044-0003ba298018"
},
"aktueladresse": {
"id": "0a3f50a4-0c39-32b8-e044-0003ba298018",
"...": "same fields as adresse"
},
"vaskeresultat": {
"variant": {
"vejnavn": "Strandvejen",
"husnr": "100",
"etage": "1",
"dør": null,
"supplerendebynavn": null,
"postnr": "2900",
"postnrnavn": "Hellerup"
},
"afstand": 2,
"forskelle": {
"vejnavn": 2,
"husnr": 0,
"postnr": 0,
"postnrnavn": 0,
"etage": 0
},
"parsetadresse": {
"vejnavn": "Strandvej",
"husnr": "100",
"etage": "1",
"postnr": "2900",
"postnrnavn": "Hellerup"
},
"ukendtetokens": [],
"anvendtstormodtagerpostnummer": null
}
}
]
}| Field | Use |
|---|---|
kategori | A, B or C for the whole wash. This is the only category in the response |
resultater | The candidates, best first. With A and B there is exactly one |
adresse.id | The address UUID. Store it on the order |
adresse.adgangsadresseid | The building entrance. Store it as well |
adresse.vejnavn etc. | The official spelling. Use it instead of the customer's |
aktueladresse | The current version of the address. Differs from adresse if the match is a retired address |
vaskeresultat.afstand | Edit distance between the input and the address. 0 is exact |
vaskeresultat.forskelle | The distance per field, e.g. vejnavn: 2. Shows what the customer typed wrong |
vaskeresultat.ukendtetokens | Words in the input that matched nothing, e.g. ["tv"] when the address has no door "tv" |
The response has no single text field. Build the line from the fields yourself, or fetch adressebetegnelse with GET /adresser/{id}.
3. Decide: accept, confirm or ask
Use the top-level kategori:
| kategori | Action |
|---|---|
| A | Accept. Store id and adgangsadresseid |
| B | Accept, but store the official text. You can show it: "We deliver to Strandvejen 100, 1., 2900 Hellerup" |
| C | Show the candidates from resultater and let the customer choose or fix the address. Never pick one automatically |
empty resultater | Ask the customer to check the address. Do not necessarily block the order |
Floor and door count in the category. If the customer types a floor or door that does not exist on the address, the wash returns C with the units that do exist as candidates.
In Python:
import os import requests def vask(betegnelse): r = requests.get( "https://api.danadresse.dk/datavask/adresser", params={"betegnelse": betegnelse}, headers={"X-Api-Key": os.environ["DANADRESSE_API_KEY"]}, timeout=5, ) r.raise_for_status() svar = r.json() resultater = svar["resultater"] if not resultater: return "ask", [] if svar["kategori"] == "A": return "accept", resultater[:1] if svar["kategori"] == "B": return "confirm", resultater[:1] return "choose", resultater[:5] # C: let the customer choose beslutning, kandidater = vask("Strandvej 100, 1., 2900 Hellerup") print(f"beslutning={beslutning}") for k in kandidater: a = k["adresse"] print(a["id"], a["vejnavn"], a["husnr"], a["etage"], a["postnr"], k["vaskeresultat"]["forskelle"])
The same logic in Node 18+:
async function vask(betegnelse) { const url = new URL("https://api.danadresse.dk/datavask/adresser"); url.searchParams.set("betegnelse", betegnelse); const res = await fetch(url, { headers: { "X-Api-Key": process.env.DANADRESSE_API_KEY }, signal: AbortSignal.timeout(5000), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { kategori, resultater } = await res.json(); if (resultater.length === 0) return { beslutning: "ask", kandidater: [] }; if (kategori === "A") return { beslutning: "accept", kandidater: resultater.slice(0, 1) }; if (kategori === "B") return { beslutning: "confirm", kandidater: resultater.slice(0, 1) }; return { beslutning: "choose", kandidater: resultater.slice(0, 5) }; } // Rentemestervej 8 has no "2. tv": the wash returns C with the units that exist const { beslutning, kandidater } = await vask( "Rentemestervej 8, 2. tv, 2400 København NV", ); console.log(`beslutning=${beslutning}`); for (const { adresse: a } of kandidater) { console.log( `${a.vejnavn} ${a.husnr}, ${a.etage ?? "-"}. ${a["dør"] ?? ""}, ${a.postnr} ${a.postnrnavn}`, ); }
And in PHP with the curl extension:
<?php function vask(string $betegnelse): array { $ch = curl_init('https://api.danadresse.dk/datavask/adresser?' . http_build_query(['betegnelse' => $betegnelse])); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('DANADRESSE_API_KEY')], ]); $svar = json_decode(curl_exec($ch), true); curl_close($ch); $res = $svar['resultater'] ?? []; if (!$res) return ['ask', []]; if ($svar['kategori'] === 'A') return ['accept', array_slice($res, 0, 1)]; if ($svar['kategori'] === 'B') return ['confirm', array_slice($res, 0, 1)]; return ['choose', array_slice($res, 0, 5)]; } [$beslutning, $kandidater] = vask('Raadhuspladsen 1, 1550 København V'); echo "beslutning=$beslutning\n"; foreach ($kandidater as $k) echo $k['adresse']['id'] . ' ' . $k['adresse']['vejnavn'] . "\n";
4. Store the ids on the order
Once the address is accepted or confirmed, store adresse.id and adresse.adgangsadresseid alongside the text. The ids are stable UUIDs from the Danish Address Register, so you can look the address up again later or enrich it. See Store the address id.
Edge cases
- Spelling. "Strandvej" and "Raadhuspladsen" give B with the official name, because there is exactly one safe match. Store the official spelling, not the customer's.
- Wrong postcode. A postcode that does not fit the street counts as a difference (
forskelle.postnr) and gives C. Show the customer the candidates and let them fix it. - Wrong house number. A number that does not exist on the street gives C with nearby numbers as candidates.
- Floor and door left out. Without floor and door the wash matches the address at entrance level if the register has one. Ask for floor and door in your form when you deliver to flats.
- New addresses. An address created today can be missing for a short while. Let the customer continue with a flag on the order so you can check it by hand.
- Timeout or error. Use a short timeout (3-5 seconds) and let the order through if the API does not answer. Validate it afterwards. A lost order costs more than an address that needs fixing.
- Key. Call datavask from your server with a normal key. A browser key also works on
/datavask/adresser, but then the customer can skip the check.
Next steps
- Help the customer before they submit: Autocomplete widget in plain JavaScript or the checkout address page.
- Store the address id and re-validate later
- Clean a CSV export from your CRM
Related guides
Markdown source Spotted an error? Tell us API-reference (Swagger)