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.

5 min Level: Intermediate ✓ Code tested

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:

  • 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.
  • 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 (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:

Shell#module-install
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.

JavaScript#init-js
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#layout-xml
<?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#csp-whitelist
<?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#observer-php
<?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.

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 →