Address validation
Checkout can send the delivery address to a third-party geocoder before the order is stored, then offer the customer a correction when one comes back. This page is for store administrators turning that check on and reading its results. The feature is off until you supply an API key of your own.
Important
Address validation never blocks a sale. A service that is down, an expired key, a spent quota or an unknown street all end with the order going through.
Before you begin
- Open an account with Mapbox or Google and create an API key. KahunaCart ships no key and proxies nothing: the calls are billed to your own account with the provider.
- For Mapbox, use a public
pk.token with no URL restriction. Geocoding needs no token scope, and a URL-restricted token answers a server-side call with a 403 because there is noRefererto check.
Turn on address validation
- In the admin, open the Commerce tab and find Address Validation.
- Set Provider to
mapboxorgoogle. - Paste your API key.
- Set Mode to
suggestto offer corrections, oroffto check and record silently. - Click Save.
The equivalent config keys are:
checkout:
address_validation:
provider: mapbox # none | mapbox | google
api_key: 'pk.…' # your own key with that provider
mode: suggest # suggest | off
Checkout now validates every distinct delivery address once per cart.
Configuration
| Key | Default | Effect |
|---|---|---|
checkout.address_validation.provider |
none |
none calls nothing. mapbox and google are the two shipped drivers. |
checkout.address_validation.api_key |
'' |
Your key with that provider. Server-side only. |
checkout.address_validation.mode |
suggest |
suggest shows the customer a panel when there is a correction. off checks and records the verdict without interrupting anybody. |
Use mode: off to find out how many of your addresses are questionable before you put a panel in front of anyone. The key never reaches the browser.
Note
A provider set without a key is treated as none, because every request would come back 401.
What the customer sees
Nothing at all, most of the time. An address the service recognises passes through in silence, and so does an address it could not reach a verdict on. When there is a correction, checkout comes back with the form still filled in and a panel at the top:
We found a close match Our address service suggests a small correction. Use whichever is right — your order goes through either way.
You entered Suggested Ada Lovelace
2595 Lucky John Dr
Park City, CO, 80301
USAda Lovelace
2595 Lucky John Drive
Park City, UT, 84060
USKeep what I entered Use this address
Both buttons are ordinary submits of the checkout form, so the panel needs no JavaScript. When the service cannot find the address at all, the same panel appears with one column and a warning heading.
One check per address
Checkout submits more than once for ordinary reasons: a physical order is two-step by design, and a lapsed coupon or an unticked terms box bounces the form. A record therefore rides the session, keyed on a SHA-256 hash of the fields that decide where a parcel goes: address lines, city, region, postcode, country.
- The same address, already decided → the recorded verdict is reused and nobody is called.
- The same address, question pending → the customer's button press is the answer, and so is pressing Place order and ignoring the panel.
- A different address → one check, one call.
Editing a name or phone number costs nothing, because those fields are not in the hash. Editing an address field earns a fresh check.
How a suggestion is applied
An accepted suggestion is merged onto what the customer typed rather than used raw. Recipient, company, phone number and any field the service left empty all stay, because a geocoder routinely drops "Flat 3".
If the suggestion comes back with a country the store's tax and shipping zones cannot key on, it is dropped and the customer's address stands.
Validation outcomes
Every driver reduces its provider's answer to one of four outcomes, and checkout treats each the same regardless of provider.
| Outcome | Panel? | Stamp | Logged? |
|---|---|---|---|
| valid | no | valid |
no |
| suggestion | yes, once — two buttons | suggested-accepted or kept-original |
no |
| invalid | yes, once — warning, keep-mine proceeds | kept-original |
no |
| unverifiable | no, the order proceeds silently | unverified |
yes, as a warning |
In mode: off no panel is ever shown. A suggestion or an invalid verdict is recorded as kept-original.
What the order records
The stored address gains a validated key in shipping_json, and in billing_json too when the customer ticked ship to the billing address.
| Value | Meaning |
|---|---|
valid |
The service recognised the address as typed. |
suggested-accepted |
A correction was offered and the customer took it. The stored address is the corrected one. |
kept-original |
A correction or warning was shown and the customer kept theirs. |
unverified |
Nobody could say — service down, timed out, key refused, quota spent, or the country is outside the provider's coverage. |
No key at all means the address predates the feature, or the feature is off. The address book never stores this key.
Watch the log for unverifiable addresses
An unverifiable outcome writes a warning to logs/grav.log:
kahunacart: address validation unavailable (mapbox), order accepted unchecked: HTTP 401: Not Authorized - Invalid Token
Watch that line. A checkout that has quietly stopped being validated, through an expired key or a spent quota, shows up here before it shows up in returned parcels.
Driver comparison
| Mapbox | ||
|---|---|---|
| Endpoint | GET https://api.mapbox.com/search/geocode/v6/forward |
POST https://addressvalidation.googleapis.com/v1:validateAddress |
| Authentication | ?access_token= query parameter |
X-Goog-Api-Key request header |
| Key type | Public pk. token, no URL restriction |
Address Validation API key |
| Countries covered | 23 | 40 |
| Input limit | — | 280 characters combined |
Country coverage
Only countries the driver covers are checked. Anything else is stamped unverified with no request made. The ISO code from the checkout country select feeds the coverage check and the API's country hint.
- Mapbox — the 23 countries Mapbox's own data coverage table marks as premium address quality: AU, AT, BE, CA, CH, DE, DK, ES, FI, FR, GB, HU, IT, JP, LI, LU, NL, NO, NZ, PL, PT, SE, US. The "basic" tier is excluded.
- Google — the 40 regions on their coverage details page: AR, AT, AU, BE, BG, BR, CA, CH, CL, CO, CZ, DE, DK, EE, ES, FI, FR, GB, HR, HU, IE, IN, IT, JP, LT, LU, LV, MX, MY, NL, NO, NZ, PL, PR, PT, SE, SG, SI, SK, US.
The Mapbox driver
The request uses structured input rather than one q string, because Mapbox's Smart Address Match needs the components typed explicitly. The parameters are address_line1, place, region, postcode, country, autocomplete=false, types=address and limit=1.
| Field | Used for |
|---|---|
properties.match_code.confidence |
exact and high mean the address stands. |
properties.match_code.<component> |
Which components Mapbox could not match. |
properties.coordinates.accuracy |
interpolated means the house number was estimated from its neighbours. |
properties.context.* |
The normalised components a suggestion is built from. |
Two edge cases, both handled: unmatched also means "you did not send this", so it is only trusted for fields checkout collected; and country: "inferred" is normal even on a perfect match, so it is never a failure signal.
An interpolated house number on an otherwise clean match is treated as valid. The normalised address is read from context.address.address_number, context.address.street_name, context.postcode.name, context.place.name, context.region.region_code and context.country.country_code, never by splitting place_formatted.
The Google driver
The request body is a PostalAddress: regionCode plus addressLines, with locality, administrativeArea and postalCode filled in when checkout has them. revision is never sent, because any value but 0 is an INVALID_ARGUMENT, and neither are recipients or organization. Input longer than Google's documented 280-character cap is refused before the request.
| Field | Used for |
|---|---|
verdict.validationGranularity |
SUB_PREMISE, PREMISE, PREMISE_PROXIMITY and BLOCK are deliverable. Anything else is a road or a region. |
verdict.hasReplacedComponents |
Google swapped a component for a different one. |
verdict.hasSpellCorrectedComponents |
A misspelling was fixed. |
verdict.possibleNextAction |
FIX or CONFIRM, when present. The field is still Preview and may be absent. |
address.missingComponentTypes |
Expected, and neither supplied nor inferable. |
address.addressComponents[].confirmationLevel |
UNCONFIRMED_AND_SUSPICIOUS on any component. |
address.postalAddress |
The post-processed address a suggestion is built from. |
Every boolean in the verdict is omitted when false, addressComponents is unordered and is only ever scanned, and postalAddress is read rather than formattedAddress because only it spells the country as a two-letter region code.
The driver follows Google's low-friction checkout example. A panel is earned by a component Google replaced, a spelling it corrected, or an explicit CONFIRM. A warning is earned by a verdict with nothing deliverable in it, a component Google says is missing, or one it calls suspicious.
Theming
The panel is templates/partials/kahunacart-address-suggestion.html.twig, painted through the same --kahunacart-* custom properties as everything else. Override the partial to restyle it.
The shipped checkout template includes it immediately after the nonce field.
{% include 'partials/kahunacart-address-suggestion.html.twig' %}
Important
If your theme overrides kahunacart-checkout.html.twig, it must include that line itself. A theme that replaces the content block replaces the include with it, and the panel never renders. Nothing breaks: the customer is never asked, their next submit settles as kept-original, and the order goes through. The Kahuna theme's checkout override needs the line adding.
The panel expects one variable, address_check, which is null on every render with no question pending.
address_check.kind 'suggestion' | 'invalid'
address_check.basis 'shipping' | 'billing'
address_check.submitted the address the customer typed
address_check.suggested the address on offer, or null
The two buttons must keep their names and values. The controller reads address_choice=suggested and address_choice=original.
Costs, quotas and privacy
- One call per distinct address per cart. Bounces, the two-step shipping flow and re-submits are all free.
- No call at all for a digital order, an empty address, an address with no street line, a country outside the driver's coverage, or a store with the feature off.
- Three seconds, total. Two seconds to connect and three overall, then the driver gives up and reports the address unverifiable.
- What leaves your server is the postal address and nothing else. Not the customer's name, not their email, not what they bought.
- What stays in the session is a SHA-256 of the address fields and a single word. The address itself is only kept while a panel is pending.
Test the drivers
Both drivers take an injected HTTP transport, so every response they can meet is reproducible from a fixture with no network.
use Grav\Plugin\KahunaCart\Address\MapboxValidator;
use Grav\Plugin\KahunaCart\Tests\Support\RecordedTransport;
$transport = RecordedTransport::returning($recordedJson);
$outcome = (new MapboxValidator('pk.test', $transport))->validate($address);
RecordedTransport::timingOut() exercises the fail-open path, and the fixtures under tests/Support/fixtures/address/ come from the providers' own published response examples.
To prove the fail-open path on a live site, set a provider with a key you know is wrong, then place an order. The order completes, and logs/grav.log says why nobody checked.