Tutorial

How to Parse US Addresses in C# / ASP.NET Core

Turn a freeform address string into clean, column-ready fields. Free API, a typed HttpClient service, and a clear map from every component to your model.

sthan.io Team
sthan.io Team
June 27, 2026 · 11 min read

You have a column called Address full of freeform strings: "1600 Pennsylvania Ave NW Apt 4B, Washington DC 20500". It is fine for display, but useless for anything structured - you can't group by city, filter by ZIP, sort by street, or de-duplicate, because every part of the address is mashed into one field.

Address parsing fixes that. You send the raw string and get back discrete components: the primary number, the street name, the suffix, the directional, the unit, the city, the state, and the ZIP - each in its own field, ready to drop into its own column.

This tutorial shows you how to parse US addresses in a C# / ASP.NET Core app using sthan.io's address API. The pattern works the same whether you build with MVC controllers, Razor Pages, minimal APIs, Blazor, or a console backfill tool.

Quick summary: Register a typed HttpClient that sends your API key as a Bearer token, call GET /v2/address-parser/usa/{address}, and read the components from the Result object - addressNumber, streetName, streetPostType, unitType, unitNumber, city, stateCode, zipCode. The free tier gives you 100 lookups/month - no credit card required.

What you'll need: .NET 6 or later (.NET 8 recommended) and a free sthan.io account. No credit card, no approval queue. The free parser tier is 100 lookups/month; paid plans start at $8/month if you need more.

Try it first

Parse any US address right here - no signup required:

Try it live

That's what you're building. Type a messy one-line address and the API hands back every component as a separate, standardized field.

What the API returns

Every response is wrapped in a standard envelope. For parsing, the Result field is a single object whose properties are the address components. This is a real response for 1600 Pennsylvania Ave NW Apt 4B:

{
  "Id": "2737f8f3-af83-4ba1-b9c3-e29d2ba9e03b",
  "Result": {
    "inputAddress": "1600 Pennsylvania Ave NW Apt 4B Washington DC 20500",
    "addressLine1": "1600 PENNSYLVANIA AVE NW",
    "addressLine2": null,
    "addressNumber": "1600",
    "streetPreDir": "",
    "streetName": "PENNSYLVANIA",
    "streetPostType": "AVE",
    "streetPostDir": "NW",
    "unitType": "apt",
    "unitNumber": "4b",
    "city": "WASHINGTON",
    "stateCode": "DC",
    "zipCode": "20500",
    "zip4": null,
    "county": null,
    "matchMode": "Speculative",
    "matchTier": "Near",
    "confidence": 0.7,
    "matchCode": {
      "houseNumber": "Matched",
      "street": "Matched",
      "unit": "Matched",
      "city": "Matched",
      "state": "Matched",
      "zipCode": "Matched",
      "zip4": "NotApplicable"
    },
    "footnotes": ["recovered: standardized via correction, not an exact match"]
  },
  "ClientSessionId": null,
  "StatusCode": 200,
  "IsError": false,
  "Errors": []
}
Casing: C# binds both halves for you. The envelope keys (Id, Result, StatusCode, IsError, Errors) are PascalCase, while the component fields inside Result are camelCase (addressNumber, streetName, streetPostType). Deserialize with JsonSerializerOptions { PropertyNameCaseInsensitive = true } and both map cleanly onto PascalCase record properties - without it, the camelCase components come back null.

The fields you'll use most often are covered in the component map below. Note that some fields are empty or null when they don't apply - there's no leading directional here, so streetPreDir is an empty string, and the parser didn't append a zip4 or county, so those are null.

Get your API key

  1. Sign up at sthan.io and subscribe to the free Address Parser tier
  2. Open your dashboard and create an API key
  3. 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

Add your base URL and API key to appsettings.json:

{
  "SthanApi": {
    "BaseUrl": "https://api.sthan.io",
    "ApiKey": "sthan_live_xxxxxxxxxxxxxxxx"
  }
}
Security tip: Don't commit a real key. For local development use .NET User Secrets (dotnet user-secrets set "SthanApi:ApiKey" "sthan_live_..."). For production use environment variables or a vault service.

Bind the section to a strongly-typed settings class:

public class SthanApiSettings
{
    public string BaseUrl { get; set; } = "https://api.sthan.io";
    public string ApiKey { get; set; } = "";
}

Build the parser service

First, define the response envelope as a generic record and a ParsedAddress record for the components. With case-insensitive matching, the PascalCase envelope and the camelCase components both bind to these PascalCase properties:

public record SthanApiResponse<T>
{
    public string Id { get; init; } = "";
    public T? Result { get; init; }
    public int StatusCode { get; init; }
    public bool IsError { get; init; }
    public List<string> Errors { get; init; } = new();
}

public record ParsedAddress
{
    public string? AddressNumber { get; init; }
    public string? StreetPreDir { get; init; }
    public string? StreetName { get; init; }
    public string? StreetPostType { get; init; }
    public string? StreetPostDir { get; init; }
    public string? UnitType { get; init; }
    public string? UnitNumber { get; init; }
    public string? City { get; init; }
    public string? StateCode { get; init; }
    public string? ZipCode { get; init; }
    public string? Zip4 { get; init; }
    public string? County { get; init; }
}

Now the service. It takes a HttpClient from the framework, URL-encodes the address, calls the endpoint with the match mode, and returns the parsed Result:

using System.Text.Json;

public class AddressParserService
{
    private readonly HttpClient _httpClient;

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };

    public AddressParserService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<ParsedAddress?> ParseAsync(
        string address, string mode = "speculative",
        CancellationToken ct = default)
    {
        var encoded = Uri.EscapeDataString(address.Trim());
        var response = await _httpClient.GetAsync(
            $"/v2/address-parser/usa/{encoded}?match={mode}", ct);
        response.EnsureSuccessStatusCode();

        await using var stream = await response.Content.ReadAsStreamAsync(ct);
        var result = await JsonSerializer
            .DeserializeAsync<SthanApiResponse<ParsedAddress>>(
                stream, JsonOptions, ct);

        if (result?.IsError == true)
            throw new InvalidOperationException(
                $"Parser error: {string.Join(", ", result.Errors)}");

        return result?.Result;
    }
}

Register the service as a typed HttpClient in Program.cs, attaching the API key as a default Authorization header so every call is already authenticated:

using System.Net.Http.Headers;
using Microsoft.Extensions.Options;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

builder.Services.Configure<SthanApiSettings>(
    builder.Configuration.GetSection("SthanApi"));

builder.Services.AddHttpClient<AddressParserService>((sp, client) =>
{
    var settings = sp.GetRequiredService<IOptions<SthanApiSettings>>().Value;
    client.BaseAddress = new Uri(settings.BaseUrl);
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", settings.ApiKey);
    client.Timeout = TimeSpan.FromSeconds(10);
});

var app = builder.Build();
app.MapControllers();
app.Run();
Why IHttpClientFactory? In a web app, never new HttpClient() per request - each instance holds its own connection pool and can lead to socket exhaustion under load. AddHttpClient<T> registers a typed client, manages the underlying handler lifetime, and injects a ready-to-use HttpClient into your service constructor.

Choose a match mode

The match parameter controls how much typo tolerance the parser applies while standardizing components. The same call supports four modes, from strictest to loosest:

ModeBehaviorUse when
strict Only confident, exact-component matches; returns little or nothing when the input is ambiguous. You only want components you can fully trust.
balanced Exact plus typo-corrected components. Returns the best parse, flagging corrected fields. Typical cleanup of user-entered addresses.
fuzzy Wider recovery for messy or partial input. Higher recall, more corrections. Backfilling a column of inconsistent legacy data.
speculative Loosest recovery, with extra tolerance for heavily misspelled street names. Best-effort parses are flagged matchTier = "Speculative". Maximum recovery / agent tooling. This is the default.

If you omit match, the endpoint defaults to speculative for the widest recovery. Whichever mode you pick, the location-defining parts of the address - the primary number, ordinal, directional, state, and the street's core name - are never substituted. A looser mode only widens tolerance for misspellings of the same street.

Map the components to your fields

Each component comes back as its own field, so mapping them to model properties or database columns is a direct copy. The fields you'll use most:

FieldMeaningExample
addressNumberPrimary (house/building) number1600
streetPreDirLeading directionalN in "N Main St"
streetNameCore street namePENNSYLVANIA
streetPostTypeStreet suffix / typeAVE, ST, BLVD
streetPostDirTrailing directionalNW
unitType / unitNumberSecondary unit designator and valueapt / 4b
city, stateCode, zipCode, zip4City, two-letter state, 5-digit ZIP, +4WASHINGTON, DC, 20500

A few more components cover edge cases: StreetPreType, StreetPreMod, StreetPreSep, and StreetPostMod capture pre/post modifiers in unusual street names (for example "Avenue of the Americas"). They're empty for ordinary addresses. The parsed record drops straight onto your own model:

var parsed = await _parser.ParseAsync(rawAddress);
if (parsed is null) return;

var entity = new CustomerAddress
{
    AddressNumber = parsed.AddressNumber,
    StreetPreDir  = parsed.StreetPreDir,
    StreetName    = parsed.StreetName,
    StreetType    = parsed.StreetPostType,
    StreetPostDir = parsed.StreetPostDir,
    UnitType      = parsed.UnitType,
    UnitNumber    = parsed.UnitNumber,
    City          = parsed.City,
    State         = parsed.StateCode,
    ZipCode       = parsed.ZipCode,
    Zip4          = parsed.Zip4
};
// entity is now ready to save — one column per component

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 AuthTokenResult
{
    public string Access_Token { get; init; } = "";
    public DateTime Expiration { get; init; }
}

public async Task<string> GetTokenAsync(CancellationToken ct = default)
{
    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);

    return result!.Result!.Access_Token;
}

You would then set the Authorization header per request with the cached token instead of attaching a static key in Program.cs. Cache it and refresh shortly before expiry. Everything else - the endpoint, the envelope, the parsing - stays the same.

Handle errors

Two status codes are worth handling explicitly so a hiccup never stalls a batch job:

  • 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 retry rather than dropping the row.
public async Task<ParsedAddress?> SafeParseAsync(
    string address, CancellationToken ct = default)
{
    try
    {
        return await ParseAsync(address, "speculative", ct);
    }
    catch (HttpRequestException ex)
        when (ex.StatusCode == HttpStatusCode.TooManyRequests)
    {
        // Rate limited — back off and let the caller retry the row
        return null;
    }
}

For production resilience you can layer retry and circuit-breaker policies onto the typed client with Microsoft.Extensions.Http.Polly, but the try/catch above is enough to ship.

What's next: confirm the parsed address is deliverable

Parsing gives you clean, structured components fast. It does not, on its own, confirm that mail or a package will actually arrive - a well-formed address can still point at a unit that no longer accepts delivery. When deliverability matters, run the address through the Address Verification API, which returns a deliverability status and appends ZIP+4 and county. It's the same envelope and the same typed-HttpClient pattern - one GET against /v2/address-verification/usa/{address}. The C# walkthrough is here: Verify US Addresses in C#.

For the full C# walkthrough covering parsing, verification, geocoding, and autocomplete together, see Integrate Address APIs in C#.

Frequently Asked Questions

Register a typed HttpClient that sends your sthan.io API key as a Bearer token, call GET /v2/address-parser/usa/{address}, and read the structured components from the Result object - addressNumber, streetName, streetPostType, unitType, unitNumber, city, stateCode, and zipCode. The full working example is in the sections above.

The free tier includes 100 lookups per month with no credit card required. Paid plans start at $8/month. There is no trial period; the free tier is permanent. See pricing for higher-volume plans.

The parser breaks a freeform address into discrete fields: addressNumber, streetPreDir and streetPostDir (leading and trailing directionals), streetName, streetPostType (the suffix like Ave or St), unitType and unitNumber, city, stateCode, zipCode, and zip4. Each field is returned separately so you can store it in its own column.

Parsing splits a freeform address into structured components and standardizes their format. Verification goes a step further and confirms the address is real and deliverable, returning a deliverability status. Parse when you need clean, column-ready fields; verify when you need to know whether mail or a package will actually arrive. See Verify US Addresses in C#.

The simplest method is an API key sent as a Bearer token: Authorization: Bearer sthan_{environment}_{key}. Create the key in your dashboard and store it in appsettings.json or .NET User Secrets. 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.

Turn messy address strings into clean fields

Parse freeform addresses into structured components with one call - free tier of 100 lookups/month, paid from $8/month, no credit card to start.

sthan.io Team
Written by sthan.io Team

The sthan.io engineering team builds and maintains address verification, parsing, geocoding, and autocomplete APIs. With deep expertise in postal addressing standards and spatial data systems, we help businesses improve address data quality and reduce failed deliveries. Questions? Reach us at [email protected].

Learn more about us