How to Integrate Address APIs in C#: Verification, Autocomplete & Geocoding
A comprehensive C# and .NET guide covering address verification, autocomplete, parsing, and geocoding with HttpClient, dependency injection, async patterns, and ASP.NET Core integration.
Prerequisites
Before you begin, make sure you have:
- .NET 6+ (LTS recommended -- .NET 6, .NET 8, or .NET 9) installed
- Visual Studio 2022+ or VS Code with the C# extension
- A Sthan.io account (free tier -- no credit card required)
- Basic familiarity with C#, async/await, and HTTP concepts
System.Text.Json is built into .NET 6+, so no additional NuGet packages are required for basic usage. For a console app, create a new project:
dotnet new console -n SthanAddressDemo
cd SthanAddressDemo
Sthan.io uses a 2-step authentication flow. First, you obtain a JWT token by calling /Auth/Token
with your profileName and profilePassword headers. Then, you use that token as a
Bearer token in the Authorization header for all subsequent API calls.
Your credentials are available in your Sthan.io dashboard after signing in.
Authentication & Reusable Client
Let's start by building a reusable SthanApiClient class that handles authentication and token caching.
This class implements IDisposable to properly clean up the underlying HttpClient.
Step 1: Define Response Models
All Sthan.io API responses are wrapped in a standard envelope. Define record types to deserialize them:
using System.Text.Json.Serialization;
// Standard API response envelope
public record SthanApiResponse<T>
{
[JsonPropertyName("id")]
public string Id { get; init; } = "";
[JsonPropertyName("result")]
public T? Result { get; init; }
[JsonPropertyName("statusCode")]
public int StatusCode { get; init; }
[JsonPropertyName("isError")]
public bool IsError { get; init; }
[JsonPropertyName("errors")]
public List<string> Errors { get; init; } = new();
}
// Auth token response
public record AuthTokenResult
{
[JsonPropertyName("access_token")]
public string AccessToken { get; init; } = "";
[JsonPropertyName("expiration")]
public DateTime Expiration { get; init; }
}
Step 2: Build the API Client
This client handles token acquisition, caching, and provides typed methods for each endpoint:
using System.Net.Http.Headers;
using System.Text.Json;
public class SthanApiClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _profileName;
private readonly string _profilePassword;
private string? _accessToken;
private DateTime _tokenExpiration;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public SthanApiClient(string profileName, string profilePassword)
{
_profileName = profileName;
_profilePassword = profilePassword;
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.sthan.io")
};
}
public async Task<string> GetTokenAsync()
{
// Return cached token if still valid (with 30s buffer)
if (_accessToken is not null
&& DateTime.UtcNow < _tokenExpiration.AddSeconds(-30))
{
return _accessToken;
}
var request = new HttpRequestMessage(HttpMethod.Get, "/Auth/Token");
request.Headers.Add("profileName", _profileName);
request.Headers.Add("profilePassword", _profilePassword);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SthanApiResponse<AuthTokenResult>>(
json, JsonOptions);
if (result?.IsError == true || result?.Result is null)
{
throw new InvalidOperationException(
$"Auth failed: {string.Join(", ", result?.Errors ?? new())}");
}
_accessToken = result.Result.AccessToken;
_tokenExpiration = result.Result.Expiration;
return _accessToken;
}
public async Task<T?> CallApiAsync<T>(string endpoint)
{
var token = await GetTokenAsync();
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SthanApiResponse<T>>(
json, JsonOptions);
if (result?.IsError == true)
{
throw new InvalidOperationException(
$"API error: {string.Join(", ", result.Errors)}");
}
return result!.Result;
}
public void Dispose() => _httpClient.Dispose();
}
Step 3: Quick Test
Verify authentication works with a simple console app:
using var client = new SthanApiClient(
"YOUR_PROFILE_NAME",
"YOUR_PROFILE_PASSWORD"
);
var token = await client.GetTokenAsync();
Console.WriteLine($"Token acquired, expires: {token[..20]}...");
Address Autocomplete
The Address Autocomplete API
returns matching US addresses as the user types. Use Uri.EscapeDataString() to safely encode the input.
Endpoint
GET /AutoComplete/USA/Address/{text}
C# Implementation
using var client = new SthanApiClient(
"YOUR_PROFILE_NAME",
"YOUR_PROFILE_PASSWORD"
);
var query = "123 Main";
var encoded = Uri.EscapeDataString(query);
var results = await client.CallApiAsync<List<string>>(
$"/AutoComplete/USA/Address/{encoded}");
if (results is not null)
{
Console.WriteLine($"Found {results.Count} suggestions:");
foreach (var address in results)
{
Console.WriteLine($" {address}");
}
}
Example Response
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"result": [
"123 Main St, Springfield, IL 62701",
"123 Main St, Anytown, CA 90210",
"123 Main St Apt 4, Boston, MA 02101"
],
"statusCode": 200,
"isError": false,
"errors": []
}
Address Verification
The Address Verification API validates, corrects, and standardizes US addresses to USPS format. It returns detailed component data including deliverability status.
Endpoint
GET /AddressVerification/Usa/Single/{address}
Response Model
public record VerificationResult
{
[JsonPropertyName("inputAddress")]
public string InputAddress { get; init; } = "";
[JsonPropertyName("streetAddress")]
public string StreetAddress { get; init; } = "";
[JsonPropertyName("city")]
public string City { get; init; } = "";
[JsonPropertyName("state")]
public string State { get; init; } = "";
[JsonPropertyName("zipCode")]
public string ZipCode { get; init; } = "";
[JsonPropertyName("zip4")]
public string Zip4 { get; init; } = "";
[JsonPropertyName("county")]
public string County { get; init; } = "";
[JsonPropertyName("dpvConfirmation")]
public string DpvConfirmation { get; init; } = "";
[JsonPropertyName("isValid")]
public bool IsValid { get; init; }
}
C# Implementation
var address = "1600 pennsylvania ave nw washington dc";
var encoded = Uri.EscapeDataString(address);
var result = await client.CallApiAsync<VerificationResult>(
$"/AddressVerification/Usa/Single/{encoded}");
if (result is not null)
{
Console.WriteLine($"Valid: {result.IsValid}");
Console.WriteLine($"Street: {result.StreetAddress}");
Console.WriteLine($"City: {result.City}");
Console.WriteLine($"State: {result.State}");
Console.WriteLine($"ZIP: {result.ZipCode}-{result.Zip4}");
Console.WriteLine($"County: {result.County}");
Console.WriteLine($"DPV: {result.DpvConfirmation}");
}
Example Response
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"result": {
"inputAddress": "1600 pennsylvania ave nw washington dc",
"streetAddress": "1600 PENNSYLVANIA AVE NW",
"city": "WASHINGTON",
"state": "DC",
"zipCode": "20500",
"zip4": "0005",
"county": "DISTRICT OF COLUMBIA",
"dpvConfirmation": "Y",
"isValid": true
},
"statusCode": 200,
"isError": false,
"errors": []
}
Address Parsing
The Address Parser API breaks a free-form US address string into structured components (street number, street name, city, state, ZIP, etc.) without validating deliverability.
Endpoint
GET /AddressParser/USA/Single/{address}
Response Model
public record ParsedAddress
{
[JsonPropertyName("streetNumber")]
public string StreetNumber { get; init; } = "";
[JsonPropertyName("preDirectional")]
public string PreDirectional { get; init; } = "";
[JsonPropertyName("streetName")]
public string StreetName { get; init; } = "";
[JsonPropertyName("streetSuffix")]
public string StreetSuffix { get; init; } = "";
[JsonPropertyName("postDirectional")]
public string PostDirectional { get; init; } = "";
[JsonPropertyName("unitDesignator")]
public string UnitDesignator { get; init; } = "";
[JsonPropertyName("unitNumber")]
public string UnitNumber { get; init; } = "";
[JsonPropertyName("city")]
public string City { get; init; } = "";
[JsonPropertyName("state")]
public string State { get; init; } = "";
[JsonPropertyName("zipCode")]
public string ZipCode { get; init; } = "";
}
C# Implementation
var address = "456 N Oak Street Apt 12 Chicago IL 60611";
var encoded = Uri.EscapeDataString(address);
var parsed = await client.CallApiAsync<ParsedAddress>(
$"/AddressParser/USA/Single/{encoded}");
if (parsed is not null)
{
Console.WriteLine($"Number: {parsed.StreetNumber}");
Console.WriteLine($"Pre-Dir: {parsed.PreDirectional}");
Console.WriteLine($"Street: {parsed.StreetName}");
Console.WriteLine($"Suffix: {parsed.StreetSuffix}");
Console.WriteLine($"Unit: {parsed.UnitDesignator} {parsed.UnitNumber}");
Console.WriteLine($"City: {parsed.City}");
Console.WriteLine($"State: {parsed.State}");
Console.WriteLine($"ZIP: {parsed.ZipCode}");
}
Example Response
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"result": {
"streetNumber": "456",
"preDirectional": "N",
"streetName": "OAK",
"streetSuffix": "ST",
"postDirectional": "",
"unitDesignator": "APT",
"unitNumber": "12",
"city": "CHICAGO",
"state": "IL",
"zipCode": "60611"
},
"statusCode": 200,
"isError": false,
"errors": []
}
Forward Geocoding
The Forward Geocoding API converts a US street address into geographic coordinates (latitude and longitude).
Endpoint
GET /Geocoding/USA/Forward/{address}
Response Model
public record GeocodingResult
{
[JsonPropertyName("latitude")]
public double Latitude { get; init; }
[JsonPropertyName("longitude")]
public double Longitude { get; init; }
[JsonPropertyName("formattedAddress")]
public string FormattedAddress { get; init; } = "";
[JsonPropertyName("accuracy")]
public string Accuracy { get; init; } = "";
}
C# Implementation
var address = "1600 Amphitheatre Pkwy Mountain View CA 94043";
var encoded = Uri.EscapeDataString(address);
var geo = await client.CallApiAsync<GeocodingResult>(
$"/Geocoding/USA/Forward/{encoded}");
if (geo is not null)
{
Console.WriteLine($"Address: {geo.FormattedAddress}");
Console.WriteLine($"Lat: {geo.Latitude}");
Console.WriteLine($"Lon: {geo.Longitude}");
Console.WriteLine($"Accuracy: {geo.Accuracy}");
}
Example Response
{
"id": "d4e5f6a7-b8c9-0123-defa-234567890123",
"result": {
"latitude": 37.4224764,
"longitude": -122.0842499,
"formattedAddress": "1600 AMPHITHEATRE PKWY, MOUNTAIN VIEW, CA 94043",
"accuracy": "Rooftop"
},
"statusCode": 200,
"isError": false,
"errors": []
}
ASP.NET Core Integration
For production ASP.NET Core applications, use IHttpClientFactory with dependency injection
instead of creating HttpClient instances directly. This avoids socket exhaustion and integrates
with the framework's service lifetime management.
Step 1: Configuration
Add your Sthan.io credentials to appsettings.json:
{
"SthanApi": {
"BaseUrl": "https://api.sthan.io",
"ProfileName": "YOUR_PROFILE_NAME",
"ProfilePassword": "YOUR_PROFILE_PASSWORD"
}
}
Create a strongly-typed settings class:
public class SthanApiSettings
{
public string BaseUrl { get; set; } = "https://api.sthan.io";
public string ProfileName { get; set; } = "";
public string ProfilePassword { get; set; } = "";
}
Step 2: Create the Service
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Extensions.Options;
public class SthanAddressService
{
private readonly HttpClient _httpClient;
private readonly SthanApiSettings _settings;
private string? _accessToken;
private DateTime _tokenExpiration;
private readonly SemaphoreSlim _tokenLock = new(1, 1);
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public SthanAddressService(
HttpClient httpClient,
IOptions<SthanApiSettings> settings)
{
_httpClient = httpClient;
_settings = settings.Value;
}
private async Task EnsureAuthenticatedAsync(
CancellationToken ct = default)
{
if (_accessToken is not null
&& DateTime.UtcNow < _tokenExpiration.AddSeconds(-30))
return;
await _tokenLock.WaitAsync(ct);
try
{
// Double-check after acquiring lock
if (_accessToken is not null
&& DateTime.UtcNow < _tokenExpiration.AddSeconds(-30))
return;
var request = new HttpRequestMessage(
HttpMethod.Get, "/Auth/Token");
request.Headers.Add("profileName", _settings.ProfileName);
request.Headers.Add(
"profilePassword", _settings.ProfilePassword);
var response = await _httpClient.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(ct);
var result = JsonSerializer
.Deserialize<SthanApiResponse<AuthTokenResult>>(
json, JsonOptions);
if (result?.IsError == true || result?.Result is null)
throw new InvalidOperationException("Auth failed");
_accessToken = result.Result.AccessToken;
_tokenExpiration = result.Result.Expiration;
}
finally
{
_tokenLock.Release();
}
}
public async Task<List<string>?> AutocompleteAsync(
string query, CancellationToken ct = default)
{
await EnsureAuthenticatedAsync(ct);
var encoded = Uri.EscapeDataString(query);
return await GetAsync<List<string>>(
$"/AutoComplete/USA/Address/{encoded}", ct);
}
public async Task<VerificationResult?> VerifyAddressAsync(
string address, CancellationToken ct = default)
{
await EnsureAuthenticatedAsync(ct);
var encoded = Uri.EscapeDataString(address);
return await GetAsync<VerificationResult>(
$"/AddressVerification/Usa/Single/{encoded}", ct);
}
public async Task<ParsedAddress?> ParseAddressAsync(
string address, CancellationToken ct = default)
{
await EnsureAuthenticatedAsync(ct);
var encoded = Uri.EscapeDataString(address);
return await GetAsync<ParsedAddress>(
$"/AddressParser/USA/Single/{encoded}", ct);
}
public async Task<GeocodingResult?> GeocodeAsync(
string address, CancellationToken ct = default)
{
await EnsureAuthenticatedAsync(ct);
var encoded = Uri.EscapeDataString(address);
return await GetAsync<GeocodingResult>(
$"/Geocoding/USA/Forward/{encoded}", ct);
}
private async Task<T?> GetAsync<T>(
string endpoint, CancellationToken ct)
{
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", _accessToken);
var response = await _httpClient.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(ct);
var result = JsonSerializer
.Deserialize<SthanApiResponse<T>>(json, JsonOptions);
if (result?.IsError == true)
throw new InvalidOperationException(
$"API error: {string.Join(", ", result.Errors)}");
return result!.Result;
}
}
Step 3: Register in Program.cs
var builder = WebApplication.CreateBuilder(args);
// Bind configuration
builder.Services.Configure<SthanApiSettings>(
builder.Configuration.GetSection("SthanApi"));
// Register typed HttpClient with IHttpClientFactory
builder.Services.AddHttpClient<SthanAddressService>((sp, client) =>
{
var settings = builder.Configuration
.GetSection("SthanApi")
.Get<SthanApiSettings>()!;
client.BaseAddress = new Uri(settings.BaseUrl);
client.Timeout = TimeSpan.FromSeconds(30);
});
var app = builder.Build();
// Map endpoints (minimal API example)
app.MapGet("/api/address/autocomplete", async (
string query,
SthanAddressService addressService,
CancellationToken ct) =>
{
var suggestions = await addressService.AutocompleteAsync(query, ct);
return Results.Ok(suggestions);
});
app.MapGet("/api/address/verify", async (
string address,
SthanAddressService addressService,
CancellationToken ct) =>
{
var result = await addressService.VerifyAddressAsync(address, ct);
return Results.Ok(result);
});
app.MapGet("/api/address/geocode", async (
string address,
SthanAddressService addressService,
CancellationToken ct) =>
{
var result = await addressService.GeocodeAsync(address, ct);
return Results.Ok(result);
});
app.Run();
Alternative: Controller-Based Approach
If you prefer MVC controllers over minimal APIs:
[ApiController]
[Route("api/[controller]")]
public class AddressController : ControllerBase
{
private readonly SthanAddressService _addressService;
public AddressController(SthanAddressService addressService)
{
_addressService = addressService;
}
[HttpGet("autocomplete")]
public async Task<IActionResult> Autocomplete(
[FromQuery] string query,
CancellationToken ct)
{
var suggestions = await _addressService
.AutocompleteAsync(query, ct);
return Ok(suggestions);
}
[HttpGet("verify")]
public async Task<IActionResult> Verify(
[FromQuery] string address,
CancellationToken ct)
{
var result = await _addressService
.VerifyAddressAsync(address, ct);
return Ok(result);
}
[HttpGet("parse")]
public async Task<IActionResult> Parse(
[FromQuery] string address,
CancellationToken ct)
{
var result = await _addressService
.ParseAddressAsync(address, ct);
return Ok(result);
}
[HttpGet("geocode")]
public async Task<IActionResult> Geocode(
[FromQuery] string address,
CancellationToken ct)
{
var result = await _addressService
.GeocodeAsync(address, ct);
return Ok(result);
}
}
Error Handling & Best Practices
1. Never new Up HttpClient Directly
In ASP.NET Core, always use IHttpClientFactory. Creating HttpClient instances directly
can lead to socket exhaustion under load because each instance manages its own connection pool.
The factory manages HttpMessageHandler lifetimes and reuses connections efficiently.
// BAD - socket exhaustion risk
using var client = new HttpClient();
// GOOD - uses IHttpClientFactory (ASP.NET Core)
builder.Services.AddHttpClient<SthanAddressService>(client =>
{
client.BaseAddress = new Uri("https://api.sthan.io");
});
// ACCEPTABLE - single static instance (console apps)
private static readonly HttpClient SharedClient = new()
{
BaseAddress = new Uri("https://api.sthan.io")
};
2. URL-Encode All Address Inputs
Addresses can contain characters that break URLs (#, &, /, spaces).
Always use Uri.EscapeDataString():
// BAD - "123 Main St #4" breaks the URL
var url = $"/AddressParser/USA/Single/{address}";
// GOOD - properly encoded
var url = $"/AddressParser/USA/Single/{Uri.EscapeDataString(address)}";
3. Token Caching with Thread Safety
In a multi-threaded web server, multiple requests might try to refresh the token simultaneously.
Use SemaphoreSlim for thread-safe token refresh (as shown in the ASP.NET Core service above):
private readonly SemaphoreSlim _tokenLock = new(1, 1);
private async Task RefreshTokenAsync(CancellationToken ct)
{
await _tokenLock.WaitAsync(ct);
try
{
// Double-check pattern: another thread may have
// refreshed while we waited for the lock
if (IsTokenValid()) return;
// ... refresh token ...
}
finally
{
_tokenLock.Release();
}
}
4. Cancellation Token Support
Always accept and pass CancellationToken through your async call chain.
This allows ASP.NET Core to cancel in-flight requests when the client disconnects:
public async Task<VerificationResult?> VerifyAddressAsync(
string address, CancellationToken ct = default)
{
// ct is passed to HttpClient.SendAsync, which will throw
// OperationCanceledException if the request is cancelled
var response = await _httpClient.SendAsync(request, ct);
// ...
}
5. Retry Policies with Polly (Optional)
For production resilience, consider adding Polly
retry and circuit breaker policies via Microsoft.Extensions.Http.Polly:
// Install: dotnet add package Microsoft.Extensions.Http.Polly
builder.Services.AddHttpClient<SthanAddressService>(client =>
{
client.BaseAddress = new Uri("https://api.sthan.io");
})
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt))))
.AddTransientHttpErrorPolicy(p =>
p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
6. Logging
Add structured logging to track API call performance and errors. Use ILogger<T>
injected via DI:
using System.Diagnostics;
public class SthanAddressService
{
private readonly ILogger<SthanAddressService> _logger;
public SthanAddressService(
HttpClient httpClient,
IOptions<SthanApiSettings> settings,
ILogger<SthanAddressService> logger)
{
_logger = logger;
// ...
}
public async Task<VerificationResult?> VerifyAddressAsync(
string address, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
try
{
var result = await GetAsync<VerificationResult>(
$"/AddressVerification/Usa/Single/{Uri.EscapeDataString(address)}", ct);
_logger.LogInformation(
"Address verified in {ElapsedMs}ms: {IsValid}",
sw.ElapsedMilliseconds, result?.IsValid);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex,
"Address verification failed after {ElapsedMs}ms",
sw.ElapsedMilliseconds);
throw;
}
}
}
Key Takeaways
- Use IHttpClientFactory in ASP.NET Core -- never
new HttpClient()in web apps - Always URL-encode address inputs with
Uri.EscapeDataString() - Cache JWT tokens and use SemaphoreSlim for thread-safe refresh
- Pass CancellationToken through your entire async call chain
- Store credentials in appsettings.json or User Secrets -- never hardcode them
- Consider Polly for retry and circuit breaker policies in production
Share This Article
Frequently Asked Questions
HttpClient instances directly
can lead to socket exhaustion under load because each instance holds its own connection pool. IHttpClientFactory
manages the underlying HttpMessageHandler lifetime and pools connections efficiently. For console apps or
short-lived processes, a single static HttpClient instance is acceptable.
Microsoft.Extensions.Http.Polly)
make this easy by configuring retry policies on your HttpClient. The free tier includes generous rate limits
suitable for development and moderate production use. Check pricing plans
for higher rate limits.
HttpClient with
IHttpClientFactory in Program.cs, inject your service via dependency injection, and call the APIs
asynchronously. This guide includes a complete ASP.NET Core integration example with IOptions configuration,
service registration, and both minimal API and controller-based approaches.
HttpClient works with Sthan.io APIs, but we recommend
.NET 6 or later (LTS versions) for the best experience. .NET 6+ includes System.Text.Json
built-in, supports top-level statements and file-scoped namespaces, and provides IHttpClientFactory
out of the box. The code examples in this guide use modern C# features available in .NET 6+.
id, result,
statusCode, isError, and errors fields. Define a generic
SthanApiResponse<T> record type that matches this structure, then use
JsonSerializer.Deserialize<SthanApiResponse<T>>(json) from System.Text.Json.
Use [JsonPropertyName] attributes to map JSON property names to C# property names.
dotnet user-secrets). For production, use environment variables,
Azure Key Vault,
AWS Secrets Manager, or your cloud provider's secret management service. In ASP.NET Core, bind credentials
to an IOptions<SthanApiSettings> configuration class loaded from appsettings.json
or environment variables.
HttpClient or IHttpClientFactory just like any
ASP.NET Core app and call Sthan.io APIs from your server-side code. For Blazor WebAssembly,
you should route API calls through your own backend to keep credentials secure, similar to any frontend framework.
The service patterns shown in this guide work directly in Blazor Server applications.
Ready to Get Started?
Get 100,000 free API calls per month. Verification, autocomplete, parsing, and geocoding. No credit card required.