Skip to content

Address normalization

Real-world address strings are messy — CRMs double-concatenate fields, dropdowns get mis-selected, country tokens get appended. TaxQL’s address and ZIP modes accept that mess: by default they normalize what is unambiguously fixable, serve the lookup, and tell you what changed so you can fix the source. Callers who prefer to fail loudly can opt into strict mode.

In the default (permissive) mode, these repairs happen before the lookup and are disclosed in the response (see Reading the disclosure):

What we detectExample inServed as
A doubled ZIP+4 suffix (even mismatched or tripled)…, WA 98040-4629-462998040-4629
A trailing country token after a ZIP…, TX 77002 USA…, TX 77002
A full state name or alias…, Houston, Texas 77002TX
A duplicated street fragment12301 Twin Creek CIR, 12301 twin creek cir, …one copy
Redundant whitespace1234 Main St, …collapsed
A state field that’s unusable or wrong for the ZIP…, Lincoln, AL 01773 (a MA ZIP)MA (see below)

The last row is the important one. When the state field and the ZIP disagree — or when the state field is an unusable placeholder like N, false, -, or MTaxQL treats the ZIP as authoritative and serves the ZIP’s state.

Why the ZIP wins: a ZIP belongs to exactly one state (it’s deterministic), state codes are hand-entered or dropdown-selected (so they carry a much higher error rate), and the ZIP is already the primary key for jurisdiction resolution. Destination-based sales tax follows the actual delivery address, and the ZIP is closer to that truth than a mistyped state field.

This applies whether the wrong state is in the address string or in the request path itself:

Terminal window
# Path says AL, ZIP is in MA → served MA, with disclosure.
curl "https://api.taxql.com/v1/tax/al?zip=01773" -H "X-API-Key: $KEY"

TaxQL only derives a state when the ZIP maps unambiguously to a single state; a rare ZIP that straddles a state line is left as-is.

When anything was normalized, the response carries a normalization block (under meta in the default simple shape, top-level in mode=full). It is absent entirely when nothing was changed, so clean input is unaffected.

{
"rate": { "combined_rate": "0.06250", "...": "..." },
"meta": {
"resolved_via": "static_statewide",
"warnings": [
"The submitted state 'AL' contradicts ZIP 01773 (which is in MA); served MA — the ZIP is authoritative for the state. If the ZIP itself is wrong, the served rate is wrong: verify the ZIP."
],
"normalization": {
"normalized": true,
"applied": [
{
"rule_id": "state_derived_from_zip",
"field": "state",
"raw": "AL",
"normalized": "MA",
"confidence": "heuristic",
"detail": { "submitted_state": "AL", "derived_state": "MA", "zip": "01773" }
}
]
}
}
}

Each applied rule carries a stable rule_id, the field it touched, the raw value received, the normalized value used, and a confidence of unambiguous or heuristic. The same human-readable message is also appended to meta.warnings.

Pass ?strict=on (also true / 1) to turn every repair into a loud rejection instead. Input that would be normalized returns 400 INVALID_INPUT with a correction block you can act on programmatically:

{
"error": {
"code": "INVALID_INPUT",
"message": "The state field 'AL' does not match ZIP 01773, which is in MA. Both are reported so you can decide which field is wrong — the ZIP or the state.",
"correction": {
"field": "state",
"raw": "AL",
"suggested": "MA",
"suggested_address": "32 Hillside Road, Lincoln, MA 01773",
"rule_id": "state_derived_from_zip",
"detail": { "submitted_state": "AL", "derived_state": "MA", "zip": "01773" },
"hint": "Correct the state to MA (or fix the ZIP if the state was right), or drop strict=on to have the ZIP's state used automatically with a disclosure."
}
}
}
strict valuebehavior
omitted, off, false, 0permissive — normalize and disclose (default)
on, true, 1strict — reject normalizable input with a suggested correction
anything else400 listing the accepted values

strict is accepted (and harmlessly ignored) in lat/lng mode, which has nothing to normalize. Use strict mode when you’d rather your pipeline surface a bad address than have TaxQL quietly correct it — for example, when you’re auditing the quality of an upstream address feed.