How to Add Address Autocomplete in Java / Spring Boot
Add US address autocomplete to your Spring Boot app in minutes. Free API, a RestClient service, a controller proxy, and a debounced front-end input 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 a Java / Spring Boot app using sthan.io's address API. The example uses Spring's RestClient, but the same proxy pattern works with RestTemplate, WebClient, or the JDK HttpClient.
Quick summary: Build aRestClientservice that sends your API key as aBearertoken, callsGET /AutoComplete/USA/Address/{text}, and returns the suggestions from theResultfield of the response envelope. A controller exposes it to the browser - the key never reaches the client. The free tier gives you 100,000 requests/month, no credit card required.
What you'll need: Java 17 or later, a Spring Boot 3.2+ project, 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. In Java you map the envelope to a record and read the result() list.
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: you send it as a Bearer token on every request and there is no separate login step. (If you prefer a short-lived token, there is a JWT flow covered later.)
Configure the project
Bind the key to a property that reads from an environment variable, so the secret never lives in source you commit:
# src/main/resources/application.properties
sthan.base=https://api.sthan.io
sthan.api-key=${STHAN_API_KEY}
STHAN_API_KEY as a real environment variable in each environment. The ${...} placeholder resolves it at startup, so the key is never written into application.properties or version control.
Build the RestClient service
First, map the response envelope to a record. The JSON keys are upper camel case, so annotate each component with @JsonProperty:
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record SthanResponse<T>(
@JsonProperty("Id") String id,
@JsonProperty("Result") T result,
@JsonProperty("StatusCode") int statusCode,
@JsonProperty("IsError") boolean isError,
@JsonProperty("Errors") List<String> errors
) {}
Now the service. A RestClient with a default Authorization header sends the API key on every call, and the {text} URI template variable is URL-encoded for you:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.List;
@Service
public class AddressService {
private final RestClient client;
public AddressService(
@Value("${sthan.base}") String base,
@Value("${sthan.api-key}") String apiKey
) {
this.client = RestClient.builder()
.baseUrl(base)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
.build();
}
public List<String> autocomplete(String query) {
SthanResponse<List<String>> body = client.get()
.uri("/AutoComplete/USA/Address/{text}", query)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
// The envelope wraps the data — suggestions are in result()
return body != null && body.result() != null
? body.result()
: List.of();
}
}
The whole integration is this one request. Everything else is plumbing to keep the key on the server and to debounce the front end.
Add the controller
The browser should call your server, and your server calls sthan.io. There are two reasons for this. 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 client-side JavaScript would expose it to anyone who opens the network tab. The controller keeps the key on the server.
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/address")
public class AddressController {
private final AddressService service;
public AddressController(AddressService service) {
this.service = service;
}
@GetMapping("/autocomplete")
public List<String> autocomplete(@RequestParam String query) {
String text = query == null ? "" : query.strip();
if (text.length() < 3) {
return List.of();
}
return service.autocomplete(text);
}
}
Your front end now has a clean URL to call: /api/address/autocomplete?query=123 main st returns a JSON array of addresses, and the API key never leaves the server.
Wire up the front-end input
The last piece is a debounced input that calls your controller. 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. Add this to a template or static page:
<input type="text" id="address" autocomplete="off"
placeholder="Start typing your address..." />
<ul id="suggestions"></ul>
<script>
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.innerHTML = "";
return;
}
// Wait 250ms after the last keystroke before calling the server
timer = setTimeout(async () => {
const res = await fetch(
`/api/address/autocomplete?query=${encodeURIComponent(query)}`);
const items = await res.json();
list.innerHTML = items.map((a) => `<li>${a}</li>`).join("");
}, 250);
});
</script>
The browser only ever talks to /api/address/autocomplete on your own domain. No key, no CORS, no third-party script. From here you can style the list, add keyboard navigation, and fill the form fields when a user clicks a suggestion.
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. You 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:
public record TokenResult(
@JsonProperty("access_token") String accessToken,
@JsonProperty("expiration") String expiration
) {}
public String fetchToken(String profileName, String profilePassword) {
SthanResponse<TokenResult> body = client.get()
.uri("/Auth/Token")
.header("profileName", profileName)
.header("profilePassword", profilePassword)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
return body.result().accessToken();
}
You would cache that token until it nears expiry and set the Authorization header per request instead of a static key. Everything else - the endpoint, the envelope, the parsing - 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.
public List<String> safeAutocomplete(String query) {
try {
return autocomplete(query);
} catch (RestClientResponseException ex) {
// 401, 429, or any non-2xx — degrade gracefully
return List.of();
}
}
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. The call is the same pattern you already built - one GET, the same envelope:
var body = client.get()
.uri("/v2/address-verification/usa/speculative/{address}", selected)
.retrieve()
.body(new ParameterizedTypeReference<SthanResponse<VerificationResult>>() {});
// body.result().deliverableStatus(), body.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. For the broader Java walkthrough across verification, parsing, and geocoding, see Integrate Address APIs in Java.
Frequently Asked Questions
Add a Spring controller backed by a RestClient service that sends your sthan.io API key as a Bearer token, calls GET /AutoComplete/USA/Address/{text}, and returns the suggestions from the Result field of the response envelope. The browser calls your controller, and your service calls sthan.io, 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.
Call it from Spring Boot, not the browser. The API does not enable CORS for browser requests, and putting your API key in client-side JavaScript would expose it to anyone viewing the page source. Add a small proxy controller: the browser calls your Spring endpoint, the service calls sthan.io with the key.
The simplest method is an API key sent as a Bearer token: Authorization: Bearer sthan_{environment}_{key}. Create the key in your dashboard and read it from application.properties bound to an environment variable. 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.
Typically under 100ms, which is suitable for real-time typeahead. Pair the calls with client-side debouncing of 200-300ms so you send one request per pause rather than one per keystroke.
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.