How to Integrate Address APIs in Java: Verification, Autocomplete & Geocoding
A complete guide to integrating address verification, autocomplete, parsing, and geocoding APIs in Java applications using HttpClient and Spring Boot.
Prerequisites
Before you begin, make sure you have:
- Java 11+ (for
java.net.http.HttpClient) - Maven or Gradle for dependency management
- A Sthan.io account (free tier includes 100,000 API calls/month)
- A text editor or IDE (IntelliJ IDEA or VS Code recommended)
Add Jackson Dependency
We use Jackson for JSON parsing. Add it to your pom.xml:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
Or if you use Gradle:
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
Sthan.io uses a 2-step authentication flow. First, you obtain a JWT token by calling
GET /Auth/Token with your profileName and profilePassword headers.
Then, you use that token as a Bearer token in subsequent API calls. The token is valid for a limited
time, so you should cache and refresh it as needed.
Authentication & Reusable API Client
Let's build a reusable SthanApiClient class that handles authentication, token caching,
and API requests using Java's built-in java.net.http.HttpClient.
Step 1: Get a JWT Token
Call GET https://api.sthan.io/Auth/Token with your profileName and
profilePassword as request headers:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SthanAuth {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.sthan.io/Auth/Token"))
.header("profileName", "YOUR_PROFILE_NAME")
.header("profilePassword", "YOUR_PROFILE_PASSWORD")
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
ObjectMapper mapper = new ObjectMapper();
JsonNode json = mapper.readTree(response.body());
// Extract the token from the wrapped response
String token = json.get("result")
.get("access_token").asText();
System.out.println("Token: " + token);
}
}
The response follows the standard Sthan.io envelope format:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"result": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiration": "2026-03-07T18:30:00Z"
},
"statusCode": 200,
"isError": false,
"errors": []
}
Step 2: Build a Reusable Client with Token Caching
For production use, create a class that caches the JWT token and refreshes it automatically:
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SthanApiClient {
private static final String BASE_URL = "https://api.sthan.io";
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private final String profileName;
private final String profilePassword;
private String cachedToken;
private Instant tokenExpiry;
public SthanApiClient(String profileName, String profilePassword) {
this.profileName = profileName;
this.profilePassword = profilePassword;
this.objectMapper = new ObjectMapper();
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
}
/**
* Get a valid JWT token, refreshing if expired.
*/
public synchronized String getToken() throws Exception {
if (cachedToken != null && tokenExpiry != null
&& Instant.now().isBefore(tokenExpiry.minusSeconds(30))) {
return cachedToken;
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/Auth/Token"))
.header("profileName", profileName)
.header("profilePassword", profilePassword)
.GET()
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException(
"Authentication failed: HTTP " + response.statusCode());
}
JsonNode json = objectMapper.readTree(response.body());
JsonNode result = json.get("result");
cachedToken = result.get("access_token").asText();
String expiration = result.get("expiration").asText();
tokenExpiry = Instant.parse(expiration);
return cachedToken;
}
/**
* Make an authenticated GET request to a Sthan.io endpoint.
*/
public JsonNode get(String path) throws Exception {
String token = getToken();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.header("Authorization", "Bearer " + token)
.timeout(Duration.ofSeconds(30))
.GET()
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException(
"API request failed: HTTP " + response.statusCode()
+ " - " + response.body());
}
return objectMapper.readTree(response.body());
}
/**
* URL-encode a string for use in API paths.
*/
public static String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
getToken() method is synchronized to ensure thread safety.
The token is refreshed 30 seconds before expiry to avoid race conditions. Create a single
SthanApiClient instance and reuse it across your application.
Address Autocomplete
The Address Autocomplete API
returns address suggestions as the user types. Call GET /AutoComplete/USA/Address/{text} with
a partial address string.
import com.fasterxml.jackson.databind.JsonNode;
public class AutocompleteExample {
public static void main(String[] args) throws Exception {
SthanApiClient client = new SthanApiClient(
"YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");
String partialAddress = "1600 pennsylvania";
String path = "/AutoComplete/USA/Address/"
+ SthanApiClient.encode(partialAddress);
JsonNode response = client.get(path);
// Extract suggestions from the result
JsonNode result = response.get("result");
if (result != null && result.isArray()) {
System.out.println("Address suggestions:");
for (JsonNode suggestion : result) {
System.out.println(" - " + suggestion.asText());
}
}
}
}
Example response:
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"result": [
"1600 Pennsylvania Ave NW, Washington, DC 20500",
"1600 Pennsylvania Ave, Hagerstown, MD 21742",
"1600 Pennsylvania Ave SE, Washington, DC 20003"
],
"statusCode": 200,
"isError": false,
"errors": []
}
Address Verification
The Address Verification API
validates and standardizes a full US address. Call GET /AddressVerification/Usa/Single/{address}
to verify deliverability and get the standardized USPS format.
import com.fasterxml.jackson.databind.JsonNode;
public class VerificationExample {
public static void main(String[] args) throws Exception {
SthanApiClient client = new SthanApiClient(
"YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");
String address = "1600 Pennsylvania Ave NW, Washington DC 20500";
String path = "/AddressVerification/Usa/Single/"
+ SthanApiClient.encode(address);
JsonNode response = client.get(path);
JsonNode result = response.get("result");
if (result != null) {
System.out.println("Verified Address:");
System.out.println(" Street: "
+ result.path("addressLine1").asText());
System.out.println(" City: "
+ result.path("city").asText());
System.out.println(" State: "
+ result.path("state").asText());
System.out.println(" ZIP: "
+ result.path("zipCode").asText());
System.out.println(" ZIP+4: "
+ result.path("plus4Code").asText());
System.out.println(" Deliverable: "
+ result.path("isDeliverable").asBoolean());
}
// Check for errors
if (response.get("isError").asBoolean()) {
JsonNode errors = response.get("errors");
for (JsonNode error : errors) {
System.err.println("Error: " + error.asText());
}
}
}
}
Example response:
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"result": {
"addressLine1": "1600 PENNSYLVANIA AVE NW",
"city": "WASHINGTON",
"state": "DC",
"zipCode": "20500",
"plus4Code": "0005",
"isDeliverable": true,
"dpvMatchCode": "Y",
"isResidential": false
},
"statusCode": 200,
"isError": false,
"errors": []
}
HttpClient.sendAsync().
Address Parsing
The Address Parser API
breaks a freeform address string into its individual components (street number, street name, city, state, ZIP).
Call GET /AddressParser/USA/Single/{address}.
import com.fasterxml.jackson.databind.JsonNode;
public class ParserExample {
public static void main(String[] args) throws Exception {
SthanApiClient client = new SthanApiClient(
"YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");
String address = "123 Main St Apt 4B, Springfield, IL 62701";
String path = "/AddressParser/USA/Single/"
+ SthanApiClient.encode(address);
JsonNode response = client.get(path);
JsonNode result = response.get("result");
if (result != null) {
System.out.println("Parsed Address Components:");
System.out.println(" Street Number: "
+ result.path("streetNumber").asText());
System.out.println(" Street Name: "
+ result.path("streetName").asText());
System.out.println(" Street Type: "
+ result.path("streetType").asText());
System.out.println(" Unit Type: "
+ result.path("unitType").asText());
System.out.println(" Unit Number: "
+ result.path("unitNumber").asText());
System.out.println(" City: "
+ result.path("city").asText());
System.out.println(" State: "
+ result.path("state").asText());
System.out.println(" ZIP Code: "
+ result.path("zipCode").asText());
}
}
}
Example response:
{
"id": "d4e5f6a7-b8c9-0123-defa-234567890123",
"result": {
"streetNumber": "123",
"streetName": "MAIN",
"streetType": "ST",
"unitType": "APT",
"unitNumber": "4B",
"city": "SPRINGFIELD",
"state": "IL",
"zipCode": "62701"
},
"statusCode": 200,
"isError": false,
"errors": []
}
Forward Geocoding
The Forward Geocoding API
converts a US address into latitude/longitude coordinates.
Call GET /Geocoding/USA/Forward/{address}.
import com.fasterxml.jackson.databind.JsonNode;
public class GeocodingExample {
public static void main(String[] args) throws Exception {
SthanApiClient client = new SthanApiClient(
"YOUR_PROFILE_NAME", "YOUR_PROFILE_PASSWORD");
String address = "1600 Amphitheatre Parkway, Mountain View, CA 94043";
String path = "/Geocoding/USA/Forward/"
+ SthanApiClient.encode(address);
JsonNode response = client.get(path);
JsonNode result = response.get("result");
if (result != null) {
double latitude = result.path("latitude").asDouble();
double longitude = result.path("longitude").asDouble();
System.out.println("Geocoding Result:");
System.out.println(" Latitude: " + latitude);
System.out.println(" Longitude: " + longitude);
System.out.println(" Formatted: "
+ result.path("formattedAddress").asText());
}
}
}
Example response:
{
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"result": {
"latitude": 37.4220656,
"longitude": -122.0862784,
"formattedAddress": "1600 AMPHITHEATRE PKWY, MOUNTAIN VIEW, CA 94043"
},
"statusCode": 200,
"isError": false,
"errors": []
}
Spring Boot Integration
For production Spring Boot applications, externalize credentials with
@ConfigurationProperties, create a @Service bean wrapping the API,
and expose endpoints through a @RestController.
Step 1: Configuration Properties
Add your API credentials to application.yml:
# application.yml
sthan:
api:
base-url: https://api.sthan.io
profile-name: YOUR_PROFILE_NAME
profile-password: YOUR_PROFILE_PASSWORD
Create the configuration properties class:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sthan.api")
public class SthanApiProperties {
private String baseUrl;
private String profileName;
private String profilePassword;
// Getters and setters
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public String getProfileName() { return profileName; }
public void setProfileName(String profileName) {
this.profileName = profileName;
}
public String getProfilePassword() { return profilePassword; }
public void setProfilePassword(String profilePassword) {
this.profilePassword = profilePassword;
}
}
Step 2: Service Class
Create a @Service that wraps all address API calls using Spring's
RestTemplate:
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
@Service
public class SthanAddressService {
private final SthanApiProperties properties;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private String cachedToken;
private Instant tokenExpiry;
public SthanAddressService(SthanApiProperties properties) {
this.properties = properties;
this.restTemplate = new RestTemplate();
this.objectMapper = new ObjectMapper();
}
/**
* Get or refresh the JWT token.
*/
private synchronized String getToken() {
if (cachedToken != null && tokenExpiry != null
&& Instant.now().isBefore(tokenExpiry.minusSeconds(30))) {
return cachedToken;
}
HttpHeaders headers = new HttpHeaders();
headers.set("profileName", properties.getProfileName());
headers.set("profilePassword", properties.getProfilePassword());
ResponseEntity<String> response = restTemplate.exchange(
properties.getBaseUrl() + "/Auth/Token",
HttpMethod.GET,
new HttpEntity<>(headers),
String.class);
try {
JsonNode json = objectMapper.readTree(response.getBody());
JsonNode result = json.get("result");
cachedToken = result.get("access_token").asText();
tokenExpiry = Instant.parse(
result.get("expiration").asText());
} catch (Exception e) {
throw new RuntimeException("Failed to parse auth token", e);
}
return cachedToken;
}
/**
* Make an authenticated GET request.
*/
private JsonNode apiGet(String path) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getToken());
ResponseEntity<String> response = restTemplate.exchange(
properties.getBaseUrl() + path,
HttpMethod.GET,
new HttpEntity<>(headers),
String.class);
try {
return objectMapper.readTree(response.getBody());
} catch (Exception e) {
throw new RuntimeException("Failed to parse response", e);
}
}
private String encode(String value) {
return UriUtils.encodePath(value, StandardCharsets.UTF_8);
}
/** Address autocomplete. */
public JsonNode autocomplete(String query) {
return apiGet("/AutoComplete/USA/Address/" + encode(query));
}
/** Address verification. */
public JsonNode verify(String address) {
return apiGet("/AddressVerification/Usa/Single/"
+ encode(address));
}
/** Address parsing. */
public JsonNode parse(String address) {
return apiGet("/AddressParser/USA/Single/" + encode(address));
}
/** Forward geocoding. */
public JsonNode geocode(String address) {
return apiGet("/Geocoding/USA/Forward/" + encode(address));
}
}
Step 3: REST Controller
Expose the address services through a @RestController so your frontend or other
services can consume them:
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.JsonNode;
@RestController
@RequestMapping("/api/address")
public class AddressController {
private final SthanAddressService addressService;
public AddressController(SthanAddressService addressService) {
this.addressService = addressService;
}
@GetMapping("/autocomplete")
public JsonNode autocomplete(
@RequestParam String query) {
return addressService.autocomplete(query);
}
@GetMapping("/verify")
public JsonNode verify(
@RequestParam String address) {
return addressService.verify(address);
}
@GetMapping("/parse")
public JsonNode parse(
@RequestParam String address) {
return addressService.parse(address);
}
@GetMapping("/geocode")
public JsonNode geocode(
@RequestParam String address) {
return addressService.geocode(address);
}
}
@Service bean is a singleton by default, so the token cache is shared
across all requests. The synchronized keyword on getToken()
ensures thread-safe token refresh under concurrent load. For reactive Spring WebFlux
applications, replace RestTemplate with WebClient.
Error Handling & Best Practices
Custom Exception Class
Create a dedicated exception for API errors:
public class SthanApiException extends RuntimeException {
private final int statusCode;
private final String responseBody;
public SthanApiException(int statusCode, String responseBody) {
super("Sthan API error: HTTP " + statusCode);
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public int getStatusCode() { return statusCode; }
public String getResponseBody() { return responseBody; }
}
Retry Logic with Exponential Backoff
Implement retry logic for transient failures (network errors, 429 rate limiting, 5xx server errors):
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class RetryHelper {
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
/**
* Execute an HTTP request with exponential backoff retry.
*/
public static HttpResponse<String> executeWithRetry(
HttpClient client, HttpRequest request) throws Exception {
Exception lastException = null;
for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
// Success or client error (don't retry 4xx except 429)
if (status < 500 && status != 429) {
return response;
}
// Rate limited or server error -- retry
if (attempt < MAX_RETRIES) {
long delay = BASE_DELAY_MS * (long) Math.pow(2, attempt);
// Add jitter to prevent thundering herd
delay += (long) (Math.random() * delay * 0.1);
Thread.sleep(delay);
}
} catch (java.io.IOException e) {
lastException = e;
if (attempt < MAX_RETRIES) {
long delay = BASE_DELAY_MS * (long) Math.pow(2, attempt);
Thread.sleep(delay);
}
}
}
throw new RuntimeException(
"Request failed after " + MAX_RETRIES + " retries",
lastException);
}
}
URL Encoding
Always URL-encode address strings before including them in the request path.
Characters like #, &, and spaces must be encoded:
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
// Always encode addresses in the URL path
String address = "123 Main St #4B, Springfield, IL";
String encoded = URLEncoder.encode(address, StandardCharsets.UTF_8);
String path = "/AddressVerification/Usa/Single/" + encoded;
Production Best Practices
-
Connection pooling:
java.net.http.HttpClientmanages a connection pool internally. Create one instance and reuse it across your application. -
Timeouts: Always set both
connectTimeout(on the client) andtimeout(on individual requests). Use 10s for connect and 30s for read. -
Thread safety:
HttpClientandObjectMapperare thread-safe. UsesynchronizedorAtomicReferencefor token caching. - Token refresh: Cache the JWT token and refresh it 30 seconds before expiry. Never request a new token for every API call.
-
Rate limiting: Respect API rate limits. If you receive HTTP 429, back off
and retry with exponential delay. Use a
Semaphoreto limit concurrent requests. - Logging: Log request/response details at DEBUG level and errors at ERROR level. Never log the full JWT token or credentials in production.
-
Batch processing: For bulk address processing, use
HttpClient.sendAsync()withCompletableFutureto make concurrent requests, controlled by a semaphore.
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;
public class BatchProcessor {
// Batch processing with controlled concurrency
public List<JsonNode> verifyBatch(
SthanApiClient client,
List<String> addresses,
int maxConcurrency) throws Exception {
Semaphore semaphore = new Semaphore(maxConcurrency);
List<CompletableFuture<JsonNode>> futures = addresses.stream()
.map(address -> CompletableFuture.supplyAsync(() -> {
try {
semaphore.acquire();
try {
return client.get(
"/AddressVerification/Usa/Single/"
+ SthanApiClient.encode(address));
} finally {
semaphore.release();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
}
Key Takeaways
- Use Java 11+
java.net.http.HttpClientfor a zero-dependency HTTP client with built-in connection pooling - Cache JWT tokens and refresh them before expiry -- never request a new token per API call
- Always URL-encode addresses with
URLEncoder.encode(address, StandardCharsets.UTF_8) - Implement retry logic with exponential backoff for transient failures and rate limiting
- For Spring Boot, use
@Service+@ConfigurationPropertiesto externalize credentials and create reusable service beans - Control batch concurrency with a Semaphore to respect API rate limits
Share This Article
Frequently Asked Questions
java.net.http.HttpClient.
It supports asynchronous requests, HTTP/2, and requires no external dependencies.
For Spring Boot applications, use RestTemplate for synchronous calls or
WebClient for reactive/asynchronous calls. Apache HttpClient is another
mature option if you need fine-grained connection pool control.
Semaphore
or rate limiter library like Bucket4j to throttle outgoing requests on your side. For batch
processing, add delays between requests and process addresses in controlled batches rather
than all at once. Check our API documentation
for specific rate limit details.
@Service that wraps the API calls, use @ConfigurationProperties to
externalize your API credentials, and inject the service into your controllers. See the
Spring Boot Integration section above for a complete working example
with RestTemplate, configuration properties, and a REST controller.
java.net.http.HttpClient, which was introduced in Java 11. The Spring Boot examples
work with Spring Boot 2.x (Java 8+) or Spring Boot 3.x (Java 17+). If you are on Java 8,
you can use Apache HttpClient or OkHttp as drop-in alternatives to java.net.http.HttpClient.
CompletableFuture with HttpClient.sendAsync()
to make concurrent requests. Control concurrency with a Semaphore to respect API rate limits.
Process addresses in batches of 10-50, add a small delay between batches, and implement retry logic
for failed requests. Cache your JWT token and refresh it only when it expires to minimize
authentication overhead. See the batch processing example above.
com.fasterxml.jackson.databind) to parse JSON responses. Create Java
POJO classes that map to the API response structure, including the outer wrapper with
id, result, statusCode, isError, and
errors fields. Use ObjectMapper to deserialize the JSON string
into your Java objects. Alternatively, use JsonNode for dynamic parsing without
creating POJOs, as shown in the examples above.
java.net.http.HttpClient is thread-safe and designed to be shared across threads.
The SthanApiClient class shown in this guide uses a single HttpClient
instance and can be safely used from multiple threads. For token caching, use
synchronized blocks (as shown) or AtomicReference to ensure
thread-safe token refresh. In Spring Boot, the service bean is a singleton by default,
which works well with the thread-safe HttpClient.
Start Building with Sthan.io
Get 100,000 free API calls per month. Address verification, autocomplete, parsing, and geocoding -- all in one platform. No credit card required.