How to Add Address Autocomplete in Vanilla JavaScript
Add US address autocomplete to any web page in minutes - no framework. Free API, a debounced input, a clickable dropdown, and a small backend proxy included.
Your checkout form collects addresses as freeform text. Users mistype street names, skip apartment numbers, and guess at ZIP codes. That bad data flows into your database, and each failed delivery costs $15-20 to re-ship.
Address autocomplete fixes the problem at the source. Users type a few characters, pick the correct address from a dropdown, and you store a postal-formatted string with the unit number and ZIP+4 already attached - ready to print on a shipping label.
This tutorial shows you how to add US address autocomplete to any web page with plain JavaScript - no framework, no build step - using sthan.io's address API. You build two small pieces: a debounced front-end script and a backend proxy that holds the key.
Quick summary: A debounced input calls your own backend; the backend sends your API key as aBearertoken toGET /AutoComplete/USA/Address/{text}and returns the suggestions from theResultfield. The key never reaches the browser. The free tier gives you 100,000 requests/month, no credit card required.
What you'll need: A text editor, Node.js 18+ for the small backend proxy, and a free sthan.io account. No credit card, no approval queue. The free tier gives you 100,000 requests/month - enough for roughly 20,000 address lookups, assuming about 5 keystrokes per lookup. Paid plans start at $7/month if you outgrow it.
Try it first
Type any partial US address - no signup required:
Try it live
That's what you're building. Type "123 main st" - lowercase, abbreviated, no city or state - and the API returns complete, postal-formatted addresses with apartment numbers, ZIP+4 codes, and proper casing.
What the API returns
The API wraps every response in a standard envelope. The address suggestions live in the Result field, which for autocomplete is a plain array of strings:
{
"Id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"Result": [
"123 Main St APT 1, Andover, MA 01810-3816",
"123 Main St APT 1, Delhi, NY 13753-1257",
"123 Main St STE 1, Caldwell, ID 83605-5476",
"123 Main St STE 1, Corinth, NY 12822-1010",
"123 Main St STE 1, Delhi, NY 13753-1258"
],
"ClientSessionId": null,
"StatusCode": 200,
"IsError": false,
"Errors": []
}
Each suggestion includes the full street, the unit designation (APT, STE, UNIT), city, state code, and ZIP+4. The API handles abbreviations (St, Ave, Blvd) and directional prefixes (N, S, E, W) on the way in, and returns clean, standardized output. Your backend reads Result and hands the array to the page, which renders it as a dropdown.
Get your API key
- Sign up at sthan.io and subscribe to the free Address Autocomplete tier
- Open your dashboard and create an API key
- Copy the key - it looks like
sthan_live_xxxxxxxxxxxxxxxx
You get the key immediately, with no approval queue. An API key is the simplest way to authenticate: your backend sends it as a Bearer token on every request. (If you prefer a short-lived token, there is a JWT flow covered later.)
Add a backend proxy
A web page runs in the browser, so it cannot safely hold a secret. There are two reasons the call must go through a server. First, the API does not enable CORS for browser requests, so a direct call from the page would be blocked. Second, and more important, putting your API key in front-end JavaScript would expose it to anyone who opens the network tab. A tiny Node server solves both and can serve your static page too:
// server.js (Node 18+ has a global fetch)
import "dotenv/config";
import express from "express";
const app = express();
app.use(express.static("public")); // serves index.html
const API_BASE = "https://api.sthan.io";
app.get("/api/address/autocomplete", async (req, res) => {
const query = (req.query.query || "").toString().trim();
if (query.length < 3) return res.json([]);
const upstream = await fetch(
`${API_BASE}/AutoComplete/USA/Address/${encodeURIComponent(query)}`,
{ headers: { Authorization: `Bearer ${process.env.STHAN_API_KEY}` } }
);
if (!upstream.ok) return res.status(502).json([]);
const body = await upstream.json();
// The envelope wraps the data — suggestions are in Result
res.json(body.Result ?? []);
});
app.listen(3000);
Store the key in a .env file (STHAN_API_KEY=sthan_live_...) and keep it out of version control. Any backend works here - PHP, Python, Ruby, .NET - the only requirement is that the key lives on the server.
Add the input and dropdown
Put a text input and an empty list in your page. The list is where suggestions will appear:
<input type="text" id="address" autocomplete="off"
placeholder="Start typing your address..." />
<ul id="suggestions" class="suggestions"></ul>
Write the debounced script
The script debounces the input, calls your backend, and renders the results. Debouncing matters: without it, "123 main st" fires eleven requests, one per keystroke. With a 250ms debounce, it fires one request after the user pauses. Building the list with textContent (rather than innerHTML) keeps the addresses safe to display:
const input = document.getElementById("address");
const list = document.getElementById("suggestions");
let timer;
input.addEventListener("input", () => {
clearTimeout(timer);
const query = input.value.trim();
if (query.length < 3) {
list.replaceChildren();
return;
}
// Wait 250ms after the last keystroke before calling the server
timer = setTimeout(() => loadSuggestions(query), 250);
});
async function loadSuggestions(query) {
const res = await fetch(
`/api/address/autocomplete?query=${encodeURIComponent(query)}`);
const items = res.ok ? await res.json() : [];
render(items);
}
function render(items) {
list.replaceChildren();
for (const address of items) {
const li = document.createElement("li");
li.textContent = address;
li.addEventListener("click", () => {
input.value = address; // fill the field on click
list.replaceChildren(); // close the dropdown
});
list.appendChild(li);
}
}
The page only ever talks to /api/address/autocomplete on your own origin. No key, no CORS, no third-party script. From here you can style the dropdown, add keyboard navigation (arrow keys plus Enter), and split the chosen address into form fields.
Alternative: JWT authentication
An API key is the simplest option and is all most apps need. If your security policy prefers short-lived credentials, the platform also supports a 2-step JWT flow. In your backend, call GET /Auth/Token once with your profileName and profilePassword headers, receive a token valid for up to 60 minutes, then send that token as the Bearer value on subsequent calls:
let cached = { token: null, expires: 0 };
async function getToken() {
if (cached.token && Date.now() < cached.expires - 30_000) {
return cached.token;
}
const res = await fetch("https://api.sthan.io/Auth/Token", {
headers: {
profileName: process.env.STHAN_PROFILE_NAME,
profilePassword: process.env.STHAN_PROFILE_PASSWORD,
},
});
const body = await res.json();
cached.token = body.Result.access_token;
cached.expires = new Date(body.Result.expiration).getTime();
return cached.token;
}
This runs entirely on the backend - the page never sees credentials of any kind. Everything else stays the same.
Handle errors
Two status codes are worth handling explicitly so a hiccup never crashes your form:
- 401 - The key or token was rejected. Check the value and, on the JWT flow, refresh and retry once.
- 429 - Rate limit reached. Back off and return what the user has typed so far rather than throwing.
async function loadSuggestions(query) {
try {
const res = await fetch(
`/api/address/autocomplete?query=${encodeURIComponent(query)}`);
render(res.ok ? await res.json() : []);
} catch {
// Network blip — show nothing rather than breaking the input
render([]);
}
}
Returning an empty list on failure means a momentary hiccup shows no suggestions rather than a broken page. The user can still type the address by hand.
What's next: confirm the address is deliverable
Autocomplete gets the user to a clean, well-formed address fast. It does not, on its own, confirm that mail or a package will actually arrive there - a suggestion can be correctly formatted yet point at a unit that no longer accepts delivery.
The natural next step is to run the chosen address through the Address Verification API at the moment the user submits the form. It returns a Delivery Point Validation (DPV) result and a deliverable status, standardizes the address to standard postal format, and appends ZIP+4 and county. Add a second backend route that calls the verification endpoint - the same pattern you already built:
const upstream = await fetch(
`https://api.sthan.io/v2/address-verification/usa/speculative/${encodeURIComponent(selected)}`,
{ headers: { Authorization: `Bearer ${process.env.STHAN_API_KEY}` } });
const { Result } = await upstream.json();
// Read Result.deliverableStatus and Result.dpvConfirmation
Address Verification has its own free tier of 100 requests/month, with paid plans from $12/month. Pairing autocomplete (volume, real-time, as the user types) with verification (one confirming call at submit) keeps your costs low and your delivery data clean. Building on a server framework? See the Node.js / Express guide for a deeper look at the proxy.
Frequently Asked Questions
Add a debounced input that calls your own backend endpoint, plus a small server-side proxy that sends your sthan.io API key as a Bearer token to GET /AutoComplete/USA/Address/{text} and returns the Result array. The page never calls sthan.io directly, so the key stays on the server.
The free tier includes 100,000 requests per month with no credit card required - roughly 20,000 address lookups assuming about 5 keystrokes per lookup. Paid plans start at $7/month. There is no trial period; the free tier is permanent. See pricing for higher-volume plans.
No. The API does not enable CORS for browser requests, and putting your API key in front-end JavaScript would expose it to anyone who views source. Route the call through a small backend proxy: the page calls your server, and your server calls sthan.io with the key.
The simplest method is an API key sent as a Bearer token: Authorization: Bearer sthan_{environment}_{key}. Keep the key in your backend's environment, never in the page. A 2-step JWT flow is also available - call GET /Auth/Token with profileName and profilePassword headers to get a token valid for up to 60 minutes.
Every response is wrapped in a standard envelope with Id, Result, ClientSessionId, StatusCode, IsError, and Errors fields. For autocomplete, Result holds an array of postal-formatted address strings - each with the unit designation, city, state code, and ZIP+4.
Debounce the input. Wait 200-300ms after the last keystroke before calling your backend, and skip queries shorter than three characters. This sends one request per pause instead of one per keystroke, which keeps you well inside the free tier.
Confirm every address before you ship
You have autocomplete wired up. Add one verification call at submit to confirm deliverability with DPV - free tier of 100 requests/month, paid from $12/month, no credit card to start.