# 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.
>
> https://danadresse.dk/en/guides/datavask-i-checkout · verified 2026-11-02

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`:

```bash id=vask-curl run=live expect="kategori"
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](/en/guides/rens-crm-csv).

## 2. Read the response

The customer typed "Strandvej" instead of "Strandvejen". The wash finds exactly one address and returns B:

```json id=vask-svar run=no
{
  "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:

```python id=vask-python run=live expect="beslutning="
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+:

```js id=vask-node run=live expect="beslutning="
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 id=vask-php run=live expect="beslutning="
<?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](/en/guides/gem-adresse-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](/en/guides/vanilla-autocomplete-widget) or the [checkout address](/checkout-adresse) page.
- [Store the address id and re-validate later](/en/guides/gem-adresse-id)
- [Clean a CSV export from your CRM](/en/guides/rens-crm-csv)
