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.

5 min Level: Intermediate ✓ Code tested

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:

Shell#vask-curl
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:

JSON#vask-svar
{
  "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
      }
    }
  ]
}
FieldUse
kategoriA, B or C for the whole wash. This is the only category in the response
resultaterThe candidates, best first. With A and B there is exactly one
adresse.idThe address UUID. Store it on the order
adresse.adgangsadresseidThe building entrance. Store it as well
adresse.vejnavn etc.The official spelling. Use it instead of the customer's
aktueladresseThe current version of the address. Differs from adresse if the match is a retired address
vaskeresultat.afstandEdit distance between the input and the address. 0 is exact
vaskeresultat.forskelleThe distance per field, e.g. vejnavn: 2. Shows what the customer typed wrong
vaskeresultat.ukendtetokensWords 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:

kategoriAction
AAccept. Store id and adgangsadresseid
BAccept, but store the official text. You can show it: "We deliver to Strandvejen 100, 1., 2900 Hellerup"
CShow the candidates from resultater and let the customer choose or fix the address. Never pick one automatically
empty resultaterAsk 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:

Python#vask-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+:

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

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 →