← Blog
🗺️

Checkout Address Autofill with the Regions API

Tutorial · 6 min read

A bad address form kills conversion. Replace four free-text fields with cascading dropdowns that cannot be filled in wrong.

The checkout form is where money leaks. Every free-text field is a chance for the user to mistype, and every wrong address is a parcel that comes back to the warehouse. The fix is not stricter validation — it is making the mistake impossible.

Four levels, four dropdowns

Indonesia's administrative regions form a clean tree: province → regency/city → district → village. Each dropdown only shows children of the previous choice, so invalid combinations never appear on screen.

# 34 provinces
GET /wilayah/provinces

# regencies within a province
GET /wilayah/regencies?province=32

# districts within a regency
GET /wilayah/districts?regency=3204

# villages within a district
GET /wilayah/villages?district=3204010

Implementing the cascade

The pattern is always the same: when a level changes, fetch the next level and clear everything below it.

// Call through your own backend — never put the API key in the browser.
async function get(path) {
  const res = await fetch('/api/proxy' + path);   // your backend forwards to the API
  return (await res.json()).data;
}

async function loadProvinces() {
  fill('province', await get('/wilayah/provinces'));
}

document.getElementById('province').onchange = async (e) => {
  clear(['regency', 'district', 'village']);
  fill('regency', await get('/wilayah/regencies?province=' + e.target.value));
};

document.getElementById('regency').onchange = async (e) => {
  clear(['district', 'village']);
  fill('district', await get('/wilayah/districts?regency=' + e.target.value));
};

document.getElementById('district').onchange = async (e) => {
  clear(['village']);
  fill('village', await get('/wilayah/villages?district=' + e.target.value));
};

function fill(id, rows) {
  const el = document.getElementById(id);
  el.innerHTML = '<option value="">Select…</option>' +
    rows.map((r) => `<option value="${r.code}">${r.name}</option>`).join('');
  el.disabled = false;
}

function clear(ids) {
  ids.forEach((id) => {
    const el = document.getElementById(id);
    el.innerHTML = '<option value="">Select…</option>';
    el.disabled = true;
  });
}
💡 Note that the code above calls /api/proxy… on your own backend, not our API directly. If the frontend calls us directly, your API key is visible to anyone who opens DevTools.

Store the code, not the name

This is the most consequential database decision in this article. Store region codes, not name strings:

CREATE TABLE addresses (
  id            BIGSERIAL PRIMARY KEY,
  user_id       BIGINT NOT NULL,
  province_code TEXT NOT NULL,   -- "32"
  regency_code  TEXT NOT NULL,   -- "3204"
  district_code TEXT NOT NULL,   -- "3204010"
  village_code  TEXT NOT NULL,   -- "3204010001"
  detail        TEXT NOT NULL,   -- "Jl. Merdeka No. 1, RT 03/RW 05"
  postal_code   TEXT
);

Codes are stable and joinable. Names are not: "Kab. Bandung", "KABUPATEN BANDUNG", and "Bandung (Kab)" are three different strings pointing at the same place, and your reports will count them as three regions.

Search, for users in a hurry

Four dropdowns are accurate but slow for a user who already knows where they live. Give them a shortcut with the search endpoint, which cuts across every level at once:

GET /wilayah/search?q=menteng&limit=5

# → district:MENTENG, village:MENTENG DALAM, village:MENTENG ATAS …

The best pattern: a search box on top, the cascade as a fallback beneath it. Users who know their address type three letters and are done; unsure users walk the tree.

The measurable payoff

After replacing free-text fields with a cascade, two metrics are worth watching: the share of parcels that fail delivery on address, and average time-to-complete on the form. The first usually drops sharply; the second often drops too, because picking from a list beats typing.

Ready to build?

Create a free account and get your API key — 5,000 free hits a month, and two endpoints that need no key at all.