Skip to content

Sales Tax Finder

In short

The Sales Tax documentation covers authentication, rate lookup by address or ZIP, the state, county, city and district breakdown in every response, error handling and copy-ready examples. Requests use the same bearer token as the rest of the Geoverio platform.

Last reviewed 11 September 2026

Key facts

Base URL
https://tax.geoverio.com
Auth scheme
Bearer token in Authorization header only
Free tier availability
Yes, no card required

Accurate US sales tax rates by address or ZIP

Base URL https://tax.geoverio.com

Overview

What Sales Tax Finder does

One GET request in, the exact US sales tax rate out. Give it any address or ZIP code and it returns the combined state + county + city + district rate, with a breakdown of every component and the data source it came from.

Perfect for Checkout pages that need the right tax at the right address Invoicing and accounting tools ERP and marketplace integrations Anything that sells across state lines

Authentication

Every request needs a key

Send your key as a bearer token in the Authorization header. That is the only accepted scheme — there is no query-string key and no cookie login.

Header Authorization: Bearer gv_live_…
Question Answer
Where do I get one? Select a plan first — the free tier counts — then open Dashboard → Projects. Keys belong to a project: open one and choose Create key. If you are not signed in yet, that link takes you through sign-in first.
What does it look like? A long string beginning gv_live_. Copy the whole thing — a truncated key returns 401.
Where do I keep it? On your server, in an environment variable. Never in browser JavaScript, a mobile app bundle, or a public repository — anyone who reads it can spend your quota.
Leaked it? Revoke it from the project that owns it under Projects and create a new one. Revoking takes effect immediately.

New here? The getting started guide walks through account, plan, key and first call step by step.

Reference

Every endpoint, explained

Copy a sample, swap in your key, and you are live. Each endpoint lists exactly what it accepts and returns.

GET /v1/tax Look up a sales tax rate

Returns the combined sales tax rate for a US address or 5-digit ZIP code.

Parameters

Name Type Required Description Example
address string required A free-form US address ("233 S Wacker Dr, Chicago IL") or just a 5-digit ZIP code ("60606"). Full addresses give rooftop-level precision; ZIPs give ZIP-level rates. 233 S Wacker Dr, Chicago, IL 60606
refresh boolean optional Set to true to skip the cache and recalculate from source data. You rarely need this — rates are refreshed automatically. false

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body "https://tax.geoverio.com/v1/tax?address=233%20S%20Wacker%20Dr%2C%20Chicago%2C%20IL%2060606&refresh=false" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY"
// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;

async function main() {
  const res = await fetch("https://tax.geoverio.com/v1/tax?address=233%20S%20Wacker%20Dr%2C%20Chicago%2C%20IL%2060606&refresh=false", {
    headers: {
      Authorization: `Bearer ${KEY}`
    }
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});
# pip install requests
import os, requests

url = "https://tax.geoverio.com/v1/tax?address=233%20S%20Wacker%20Dr%2C%20Chicago%2C%20IL%2060606&refresh=false"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}

r = requests.get(url, headers=headers)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())
<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');

$ch = curl_init("https://tax.geoverio.com/v1/tax?address=233%20S%20Wacker%20Dr%2C%20Chicago%2C%20IL%2060606&refresh=false");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));
See a sample response
{
    "query": "233 S Wacker Dr, Chicago, IL 60606",
    "cached": true,
    "geo": {
        "city": "Chicago",
        "state": "IL",
        "county": "Cook",
        "latitude": 41.878916229496,
        "zip_code": "60606",
        "longitude": -87.636602795305,
        "county_fips": "17031",
        "input_query": "233 S Wacker Dr, Chicago, IL 60606",
        "matched_address": "233 S WACKER DR, CHICAGO, IL, 60606"
    },
    "rate": {
        "notes": "",
        "state": "IL",
        "source": "il_official_address_file",
        "status": "resolved",
        "city_name": "Chicago",
        "city_rate": null,
        "districts": [],
        "confidence": "zip-level",
        "local_rate": 0.0425,
        "state_name": "Illinois",
        "state_rate": 0.0625,
        "total_rate": 0.105,
        "county_name": "Cook",
        "county_rate": null,
        "federal_rate": 0,
        "jurisdiction": "60606",
        "effective_date": "",
        "official_source_url": "https:\/\/tax.illinois.gov\/research\/taxrates\/machine-readable-file-address-specific.html",
        "special_district_rate": null,
        "breakdown": [
            {
                "label": "Illinois State",
                "rate": 0.0625,
                "percent": "6.25%"
            },
            {
                "label": "Local",
                "rate": 0.0425,
                "percent": "4.25%"
            }
        ],
        "summary": "Total Sales Tax = 10.5%\n  Illinois State: 6.25%\n  Local: 4.25%\nJurisdiction: 60606 (confidence: zip-level)",
        "total_percent": "10.5%"
    },
    "fetched_at": "2026-07-15T18:05:55.424828+00:00",
    "extra": {
        "cache_key": "IL|60606|233swackerdrchicagoil60606",
        "next_refresh_due": "2026-10-01T00:00:00+00:00",
        "coverage": {
            "mechanism": "state_file",
            "needs_key": false,
            "status": "live"
        }
    }
}

Response fields

What every field means

No mystery keys. Here is a plain-English explanation of everything the API sends back.

rate.total_rate
The number you want: the combined rate as a decimal. 0.105 means 10.5% — multiply it by the sale amount. This already includes state, county, city and any special districts.
rate.total_percent
The same figure pre-formatted for display ("10.5%"), so you never have to decide how many decimal places to round to.
rate.breakdown
A display-ready list of the components that add up to the total, each with label, rate and percent — for example [{"label":"Illinois State","rate":0.0625,"percent":"6.25%"},{"label":"Local","rate":0.0425,"percent":"4.25%"}]. This is what to render on an invoice line.
rate.state_rate / local_rate
The state component and everything local (county + city + districts) rolled together, as decimals.
rate.city_rate / county_rate / special_district_rate / districts
The local component split out, when the state publishes it that way. These are null where the source only gives a combined local figure — that is a property of the state, not a gap in the data, and local_rate is still correct.
rate.federal_rate
Always 0. The US has no federal sales tax; the field exists so the same parsing code works if you ever point it at a country that does.
rate.confidence
How precisely we pinned the location down: "rooftop" (exact address match), "zip-level" (ZIP average) or "state-base" (fallback to the state rate). Pass a full street address rather than a bare ZIP to get rooftop.
rate.status
"resolved" means a real calculated rate you can charge. "manual_lookup" means the jurisdiction cannot be automated and we hand you official_source_url instead of guessing a number.
rate.jurisdiction / state / state_name / city_name / county_name
Which taxing jurisdiction the rate belongs to, in both code and human-readable form.
rate.source / official_source_url / effective_date / notes
The audit trail: which government dataset the rate came from, the public URL of that source, the date the rate took effect, and any caveat the source attaches. Keep official_source_url with your records — it is what you show an auditor.
rate.summary
The whole answer as a ready-to-print multi-line string, including the components and the confidence. Useful for logs, receipts and support tickets.
geo
How we understood the address: matched_address (what we actually matched), input_query (what you sent), city, county, county_fips, state, zip_code, latitude and longitude. Compare matched_address with your input to catch typos before you charge someone.
query
The address string exactly as you sent it, echoed back so a queued or batched job can match responses to requests.
cached
true when the answer came from the lookup cache (sub-millisecond). Caching does not make a rate less current — see the notes below.
fetched_at
When the underlying rate was last pulled from the government source, as an ISO 8601 timestamp. This is the age of the data, not the age of your request.
extra
Operational detail: cache_key, next_refresh_due (when we will next re-read the source), and coverage {status, mechanism, needs_key} describing how this state is sourced. Safe to ignore in normal use.

Good to know

Getting the rate right

Five things worth knowing before you charge a customer based on this API.

Send the full address, not just the ZIP
A ZIP code is a mail-delivery route, not a tax boundary, and plenty of ZIPs straddle two or three jurisdictions. Sending "60606" returns the ZIP average and confidence "zip-level"; sending "233 S Wacker Dr, Chicago, IL 60606" returns the exact rate for that building and confidence "rooftop". If you are charging real money, send the whole address — you already have it at checkout.
Always check rate.status before you charge
A handful of jurisdictions publish rates in a form nobody can automate reliably. Rather than invent a plausible number, those come back with status "manual_lookup" and an official_source_url. Treat anything other than status "resolved" as "do not auto-charge" — branch on it once and you will never be surprised by a wrong rate in production.
Cached does not mean stale
cached: true only means we did not recompute the answer for this request. Rates are re-read from the government sources on a schedule (extra.next_refresh_due tells you when this one is next due) and the cache is dropped the moment a source changes, so a cached answer is the same answer a fresh one would give. The ?refresh=true parameter exists for debugging; you do not need it in normal operation.
Every rate traces back to a government source
The API never estimates or interpolates a rate. Each answer carries the source dataset and its public URL, so a number you charged can always be traced back to the state or county publication it came from. Store official_source_url and fetched_at with your transaction records and your sales-tax audit trail is complete.
Pair it with Address Autocomplete
Every Address Autocomplete suggestion carries a tax_query field: a normalised address string built specifically to be passed straight into this API. Let the customer pick their address from the typeahead, then send suggestion.tax_query here — you get a rooftop-confidence rate and the customer never typed an address you had to guess at.

Errors

When things go wrong

Every failure is JSON, and every failure carries the same field. Here is the shape, then every status code you can actually receive.

The error envelope

The API returns one field, detail, with a human-readable explanation. Parse that and you have handled every error on every endpoint.

401 https://tax.geoverio.com
{
  "detail": "Invalid API key"
}
Status Name What to do
401 Missing or invalid API key The Authorization header is missing, malformed, or the key does not exist. Double-check you copied the whole key, including the gv_live_ prefix.
403 Key not allowed Your key is valid but not allowed to call this API — usually the key was revoked or your plan does not include this module.
422 Invalid parameters A required parameter is missing or out of range. The response body tells you exactly which one.
429 Rate limit exceeded You sent too many requests this minute. Check the X-RateLimit-Reset header for when the window resets, then retry.
400 Bad request The query string or JSON body could not be parsed — an unencoded character, a truncated body, or the wrong Content-Type. Fix the request; retrying it unchanged will fail again.
404 No such endpoint The path does not exist on this API. Check the base URL and the version prefix — every path on this page starts with /v1.
405 Wrong method Right path, wrong verb — for example a GET against an endpoint documented as POST. The reference above lists the verb for every endpoint.
429 Monthly quota exhausted Different from the rate limit above: your plan's allowance for the calendar month is gone, so waiting a minute will not help. X-RateLimit-Remaining is 0 and X-RateLimit-Reset points at the start of the next period. Upgrade your plan or wait for the reset.
500 Internal error Something failed on our side. The request was not your fault and was not billed — retry with backoff.
502 Bad gateway An upstream hop failed mid-request. Safe to retry with backoff.
503 Temporarily unavailable The API is draining or deploying. Safe to retry with backoff; honour Retry-After when it is present.
504 Upstream timeout The request took longer than the gateway allows. Safe to retry with backoff — and make sure your own client timeout is generous enough not to abandon a request the API is still answering.

Retrying safely

4xx means stop. The request is wrong and will stay wrong — fix it rather than repeating it. The one exception is 429: wait for the window in X-RateLimit-Reset and try again.

5xx means retry, but back off. Every endpoint on this page is a read, so a retry is safe. Wait 1s, then 2s, then 4s, then 8s — up to four attempts — and add a random 0–250 ms jitter so a fleet of your servers does not retry in lockstep. Give up after that and surface the failure; honour Retry-After whenever the response carries one.

Limits

How much you can call, and how fast

Two separate budgets, and you can hit either one. Quota is a monthly total, counted per account — every key in every project spends the same pool. Rate is a per-minute ceiling on how quickly you spend it. Both are reported on every response by the X-RateLimit-* headers, so your code can see either limit coming before it arrives.

Plan Requests / month Requests / minute API keys
Free 1,000 30 1
Pay As You Go Metered — no monthly cap 600 5
Starter 100,000 300 3
Growth 1,000,000 1,200 10
Scale 5,000,000 5,000 50

Pay As You Go is metered rather than capped: it has no monthly allowance to exhaust, it draws on a prepaid balance, and it stops when that balance runs out. Every other plan returns 429 with X-RateLimit-Remaining: 0 once its monthly quota is spent, until the next period begins. Full pricing lives on the pricing page.

Rate limits

Headers that keep you informed

Check these response headers to see exactly how much room you have left — before you ever hit a limit.

Header Meaning
X-RateLimit-Limit Your plan's requests-per-minute allowance.
X-RateLimit-Remaining Requests you have left in the current minute.
X-RateLimit-Reset Unix timestamp when the window resets.

Ready for your own key?

Create a free account, mint a key, and try every endpoint live from your dashboard’s Test APIs page.

Frequently asked questions

What does the Sales Tax Finder API do?
Accurate US sales tax rates by address or ZIP.
How do I authenticate with the Sales Tax Finder API?
Send your key as a bearer token: `Authorization: Bearer <your key>`. One key works across every Geoverio API your plan includes.
Is there a free tier?
Yes. Every plan reaches every API; the free tier differs on monthly volume and rate limit, not on which endpoints you may call.