# Address autocomplete in the Magento 2 checkout

> Install the Danadresse module for checkout autocomplete and an address check on every order, or attach the widget yourself with a theme script and a CSP whitelist.
>
> https://danadresse.dk/en/guides/magento · verified 2026-11-02
There is a Magento module for Danadresse: `lynbro/module-danadresse` (module name `Lynbro_Danadresse`, MIT licence) for Magento Open Source and Adobe Commerce 2.4.4–2.4.8 on PHP 8.1–8.4. It gives the checkout and the customer address book address autocomplete, fills street, postcode, city and the Danish region, and checks the address of every new order. If you cannot install modules, the second half of this guide shows how to attach the widget with your own theme code.

## 1. Create two keys

Under [Keys](/dashboard/keys):

- a **browser key** for the shop's pages. Add the shop's domains under **Allowed domains**, e.g. `myshop.dk, www.myshop.dk`, and the staging domain if you test there. A domain-locked key only works on those domains, so it can sit in the page source. See [Browser keys and allowed domains](/en/guides/browser-noegler-domaener).
- a **server key** without domains for the order check. Magento stores it encrypted, and it never reaches the browser.

## 2. Install the module

Download `lynbro-module-danadresse-<version>.zip` from the [download page](/en/integrationer/download#magento2) (version, SHA-256 and changelog are listed there). Composer reads the zip directly as an artifact repository — don't unpack it: put it in an `artifacts/` folder in the Magento root, run `composer config repositories.danadresse artifact ./artifacts` and `composer require lynbro/module-danadresse`. Without Composer, unpack the zip into `app/code/Lynbro/Danadresse/`. Then run:

```bash id=module-install run=no
bin/magento module:enable Lynbro_Danadresse
bin/magento setup:upgrade
bin/magento setup:di:compile                          # production mode only
bin/magento setup:static-content:deploy da_DK en_US   # production mode only
bin/magento cache:flush
```

`setup:upgrade` adds two nullable columns to `sales_order` (`danadresse_status`, `danadresse_suggestion`) and one to `sales_order_grid`. Nothing else is changed.

## 3. Configure

Go to **Stores → Configuration → LynBro → Danadresse — danske adresser**:

- **General**: switch on *Enable address autocomplete* (off by default) and paste the browser key.
- **Storefront**: autocomplete in checkout, in the customer's address book and at registration, the check of typed addresses ("Did you mean …?") and *Fill city from postcode* are on by default.
- **Order validation**: switch on *Validate addresses of new orders*, paste the server key, and choose shipping, billing or both. The result is added as a private order comment. The default timeout is 3 seconds.

All settings can be set per website or store view, e.g. only on the Danish store.

The module allows `https://api.danadresse.dk` in the checkout's Content Security Policy itself, and the script is served from your own static files.

## 4. Test

1. Put a product in the cart and open the checkout.
2. Type `Rentemestervej 8` in *Street Address* and pick the suggestion.
3. Postcode `2400`, city `København NV` and the region *Hovedstaden* are filled.
4. Place the order. Under **Sales → Orders** the column *Address check* says *Valid*.

`API refused the request (403)` in the browser console means the shop's domain is missing from the browser key. `401` means the key is missing or mistyped. *Not checked (API error)* on an order means the server key is wrong or the API did not answer within the timeout.

With a Hyvä theme the module works on the theme pages (address book, registration) and in the Luma checkout fallback. On Hyvä Checkout (Magewire) the script finds the fields by name and `autocomplete` attributes, but it has not been tested on a live Hyvä Checkout. Test it before you go live.

## Without the module: your own theme code

Magento 2's default checkout (Luma) renders the address fields with Knockout after the page has loaded. The fields are plain input elements, so the widget can attach to them, but the script has to wait for them and fire a `change` event so Knockout stores the value. From Magento 2.4.7 the checkout page also runs with a strict Content Security Policy, so our domains have to be allowed, and Denmark has required regions: the code below does not set the region, so the customer picks it.

### The init script

Put the file in your theme as `app/design/frontend/<Vendor>/<theme>/web/js/danadresse-init.js`.

```js id=init-js run=syntax
const API_KEY = "YOUR_BROWSER_KEY";

function setField(el, value, events) {
  if (!el || value == null) return;
  const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value").set;
  setter.call(el, value);
  for (const type of events || ["input", "change"]) {
    el.dispatchEvent(new Event(type, { bubbles: true }));
  }
}

function streetLine(a) {
  let line = a.vejnavn + " " + a.husnr;
  if (a.etage) line += ", " + a.etage + ".";
  if (a["dør"]) line += " " + a["dør"];
  return line;
}

function bind(input) {
  const form = input.form || document;
  DanadresseAutocomplete.attach(input, {
    apiKey: API_KEY,
    onSelect(a) {
      // change makes Knockout update its observable
      setField(input, streetLine(a), ["change"]);
      setField(form.querySelector('input[name="postcode"]'), a.postnr);
      setField(form.querySelector('input[name="city"]'), a.postnrnavn);
    },
  });
}

function bindAll() {
  document.querySelectorAll('input[name="street[0]"]').forEach(bind);
}

function start() {
  bindAll();
  // The shipping and billing address are rendered later and may be rendered again.
  new MutationObserver(bindAll).observe(document.body, { childList: true, subtree: true });
}

if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
else start();
```

`street[0]`, `postcode` and `city` are the field names in the Luma checkout for both the shipping and billing address. The code fills postcode and city in the form the customer is typing in.

### Load the scripts on the checkout page

Create `Magento_Checkout/layout/checkout_index_index.xml` in your theme:

```xml id=layout-xml run=no
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <script src="https://danadresse.dk/static/js/danadresse-autocomplete-1.0.0.js" src_type="url"/>
        <script src="js/danadresse-init.js"/>
    </head>
</page>
```

Run `bin/magento setup:static-content:deploy` and flush the cache.

### Allow the domains in the CSP

Without this file Magento blocks the script or the API calls on the checkout page. CSP whitelists live in a module, not a theme, so put it in a small module, e.g. `app/code/Vendor/Danadresse/etc/csp_whitelist.xml`:

```xml id=csp-whitelist run=no
<?xml version="1.0"?>
<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd">
    <policies>
        <policy id="script-src">
            <values>
                <value id="danadresse-script" type="host">https://danadresse.dk</value>
            </values>
        </policy>
        <policy id="connect-src">
            <values>
                <value id="danadresse-api" type="host">https://api.danadresse.dk</value>
            </values>
        </policy>
    </policies>
</csp_whitelist>
```

Test in the checkout with the browser console open. A CSP error names the domain that is missing. `403 origin_not_allowed` means the domain is missing from the key.

### Check the address when the order is saved

In the same module an observer on `sales_order_place_before` (registered in `etc/events.xml`) can check the shipping address with the server key. The example does not stop the order. It writes a comment on the order so someone can look at it.

```php id=observer-php run=syntax
<?php
declare(strict_types=1);

namespace Vendor\Danadresse\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

class CheckAddress implements ObserverInterface
{
    public function execute(Observer $observer): void
    {
        $order = $observer->getEvent()->getOrder();
        $address = $order->getShippingAddress() ?: $order->getBillingAddress();
        if (!$address || $address->getCountryId() !== 'DK') {
            return;
        }
        // Street and number (before the first comma) + postcode: checks the building
        $street = trim((string) strtok((string) ($address->getStreet()[0] ?? ''), ','));
        $url = 'https://api.danadresse.dk/datavask/adgangsadresser?'
            . http_build_query(['betegnelse' => $street . ', ' . $address->getPostcode()]);

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 5,
            CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('DANADRESSE_API_KEY')],
        ]);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($body === false || $status !== 200) {
            return; // the API did not answer: the order goes through
        }
        $vask = json_decode($body, true);
        if (($vask['kategori'] ?? '') === 'C') {
            $order->addCommentToStatusHistory('Danadresse: address could not be verified (category C).');
        }
    }
}
```

The categories are explained in [Address cleansing in checkout](/en/guides/datavask-i-checkout).

## Next steps

- The [Magento page](/en/integrationer/magento) on Integrations
- [Browser keys and allowed domains](/en/guides/browser-noegler-domaener)
- [Address cleansing in checkout](/en/guides/datavask-i-checkout)
