Skip to content

US tax dataset (us-tax-data)

US tax dataset (us-tax-data)

US sales tax is powered by the compiled us-tax-data dataset (schema version 4), a dated compilation covering all 51 jurisdictions. It is enabled by default and replaces the hardcoded US entries that the static tables used to ship — the static snapshot now carries non-US rates only. The rest of the world is unaffected.

Where the numbers come from, honestly

The two planes have different provenance, and the difference matters because most callers get the weaker one.

Plane Source Primary?
Local rate records The SST Governing Board's own boundary/rate files for 24 states, plus each state's revenue department directly — CDTFA, AZ DOR, FL DOR, HI, IL, LA, MO DOR, MS DOR, NY, PA, AL DOR, and the ARSSTC for Alaska Yes — these are the taxing authorities
The 51 state-level rates A single Tax Foundation table, State and Local Sales Tax Rates, Midyear 2026 No — a think-tank compilation
Taxability, nexus, sourcing Practitioner compilations, cross-checked; see the pages for each No

So the rooftop path rests on primary sources; the state rate — which is what you get in the 12 states with no rooftop path — rests on a secondary one. That is the same footing the EU VAT dataset is on, and it is described the same way there.

Treat the state rates as a good, refreshable default and re-verify against each state's own guidance before relying on them for a filing.

Licence — the engine is MIT, the data is not

This package is MIT. The dataset it fetches by default is not: cboxdk/us-tax-dataset is published under the PolyForm Internal Use Licence 1.0.0, and it is enabled by default — so a fresh install is already using it.

You may You may not, without a separate licence from Cbox
Use it for your own business, commercially Redistribute the dataset
Compute the tax on your own sales Bundle it into something you ship
Charge your customers that tax Offer a rate lookup, an API, or a product feature that gives your customers the rates

The line the licence draws is between charging your customers tax you computed with this data — which is the intended use and entirely fine — and telling your customers what the rates are, which is distribution and is not covered.

If you are building the latter, set TAX_US_DATASET=false and bind your own TaxRateSource. Note what that costs you: see the disabled-dataset path below, which is a narrow escape hatch rather than an equivalent mode.

The EU VAT dataset is a different story — that one is ours, and documented as such.

The four planes it supplies

Plane Contract bound to it What the dataset provides
Rates TaxRateSource (UsTaxDatasetRateSource) Per-state rate; a reduced rate for categories a state reduces (e.g. grocery); rooftop all-in when a locality is resolved
Taxability ProductTaxability (UsTaxDatasetTaxability) Per-state, per-category taxable/exempt across 25 product categories
Nexus NexusThresholds (UsTaxDatasetNexus) Per-state economic-nexus dollar/transaction thresholds
Sourcing SourcingRules (UsTaxDatasetSourcing) Per-state intrastate origin/destination/mixed sourcing rule

Each source answers only for the US and defers otherwise: the rate source returns null for non-US jurisdictions (so a composed ChainTaxRateSource falls through to the EU/national sources), and the taxability source delegates non-US — and US pairs the dataset leaves undetermined — to the static fallback matrix.

Configuration

// config/tax.php
'us_tax_data' => [
    'enabled'  => env('TAX_US_DATASET', true),
    'location' => env('TAX_US_DATASET_LOCATION', 'https://raw.githubusercontent.com/cboxdk/us-tax-dataset/main'),
    'ttl'      => (int) env('TAX_US_DATASET_TTL', 86400),
    'rooftop'  => env('TAX_US_DATASET_ROOFTOP', false),
],

location is an http(s) base URL (the public dataset mirror) or a local directory, under which the split files live at by-section/<section>.json. Only the small baseline, taxability, nexus and sourcing sections are fetched for the common state-level path; the bulky rates section (every local record) and a state's boundaries/US-XX.json index are read lazily and only when a rooftop locality is resolved. Fetched sections are cached for ttl seconds. Pin location at a tagged release or a committed local copy for an offline/deterministic build.

Disabling it is a narrow escape hatch, not an equivalent mode. Rates fall back to the static snapshot and nexus to the shipped 51-state table, but taxability does not: the static matrix carries only the curated per-state digital_service map plus whatever overrides you supply. General tangible goods (Standard) still resolve, and every other US category refuses — clothing, grocery, candy, prewritten software and the rest all raise UnresolvedProductTaxability.

That is deliberate. The alternative is inventing 25 categories × 51 states of determinations this package has no source for, which is the one thing it will not do. If you need US taxability offline, mirror the dataset to a local directory and point location at it rather than disabling it.

Deny-by-default holds throughout: any transport/read/parse failure yields a null/empty result, so the engine denies rather than guessing.

Price thresholds are dollar figures, and are treated as such

Three states exempt clothing below a per-item price — Massachusetts at $175, New York at $110, Rhode Island at $250 — and two mechanics apply once an item reaches the line. Massachusetts and Rhode Island tax only the amount over it; New York taxes the whole item, the first $110 included. The dataset publishes the mechanic alongside the figure, and a rule carrying one without the other is refused rather than guessed.

Those figures are USD, because they are numbers in state statutes. Assessing a threshold category against a line denominated in something else throws ThresholdCurrencyMismatch:

// A ¥20,000 jacket shipped to New York.
$calculator->assess($query);   // throws ThresholdCurrencyMismatch

Comparing them would need an exchange rate on the supply date, and the rate chosen would decide whether the line is taxed at all — so this package will not pick one. Convert the line to USD before assessing it; your accounting already has a rate, and it is the right one to use.

The refusal is scoped to categories whose taxability actually turns on price. Everything else is assessed in whatever currency you bill in.

Sales tax holidays

Fifteen states hold a back-to-school weekend in the 2026 calendar, and the engine applies them automatically from the supply's date. A $80 shirt in Texas on 8 August is exempt; the same shirt on the 6th is taxed.

The cap is all-or-nothing, and it is the opposite of the thresholds above. Massachusetts exempts the first $175 of any coat all year and taxes the rest. A holiday cap qualifies the whole item or none of it: at a $100 cap, a $100 coat is untaxed and a $101 coat is taxed on all $101. Do not read the two figures the same way.

Caps are per state, not shared — Ohio caps clothing at $75 the same weekend Texas caps it at $100, so an $80 shirt is exempt in one and taxed in the other.

What is modelled, and what is charged anyway

Only clothing, footwear and books. Everything else a holiday covers is charged normally, which over-collects for a weekend — refundable and visible, where exempting a supply the state taxes is neither. The reasons are recorded per item type in the dataset overlay:

Not modelled Why
School supplies No TaxClass expresses it
Computers TaxClass::Electronics is broader — it covers televisions, which the holidays tax
Energy Star Turns on a certification mark, not a product class
Firearms No class, and those holidays are uncapped, so a wrong mapping exempts without limit
Buyer status Nevada's National Guard weekend and New Mexico's Small Business Saturday turn on who is buying or selling

Five states are omitted whole, each with its reason in the overlay — Massachusetts because its holiday covers tangible personal property generally, with statutory exclusions this has not sourced.

Illinois is omitted for a different reason, and it is the instructive one. Its back-to-school week is a rate reduction, not an exemption: the state share drops from 6.25% to 1.25% and every local authority keeps charging in full. Modelled as a holiday it would have zeroed the whole line — the remaining state share and the entire local stack, which is around nine points in Chicago. A holiday that removes the wrong tax under-collects, and that is the direction no later refund fixes. It returns when the dataset can express a reduced state share alongside untouched locals.

A line billed in something other than USD is charged, not refused. The caps are dollar figures in state statutes, and comparing another currency needs an exchange rate on the supply date. The threshold path throws on that; this one does not, because a holiday is a few days of relief and refusing the assessment would break a checkout for a perfectly taxable supply.

The calendar is republished yearly. A year that has not been sourced yet means null, and the engine charges normally — the same safe direction as an unmodelled item.

Rate precision: state level, with reduced-rate and rooftop refinements

The dataset carries every local rate, but the engine resolves jurisdictions to the state (see geocoding). So the rate source returns:

  1. A reduced STATE SHARE when a state reduces a category (Missouri groceries at 1.225%, Tennessee's at 4%). This replaces the state share only — it does not replace the local ones, because both states' own guidance is explicit that local sales taxes still apply to food. With a rooftop locality resolved, the reduced share is stacked with each locality's food rate (foodDrugRate, which the dataset carries separately from the general local rate — a Tennessee city may levy 2.75% generally and 2.25% on food, and some exempt food locally). Without one, the reduced share alone is returned at Confidence::Derived, exactly as the general state rate is: a partial answer, labelled as one.
  2. A rooftop all-in rate when the jurisdiction carries a LocalityCode: EVERY applicable local record is summed and the state share added per the state's rate basis (component adds it; combined records are already all-in), at Confidence::Authoritative. Which records apply comes from the boundary index — see below.
  3. Otherwise the state rate, at Confidence::Derived — honest that it is the state share, not a rooftop all-in figure.

Rooftop: ZIP+4 into the boundary index

Setting us_tax_data.rooftop lets the Geocodio adapter capture the address's full ZIP+4 as a locality (scheme zip9, e.g. 66101-3064). A ZIP+4 is a postal key, not a taxing authority — the dataset's boundary index turns it into the authorities that actually apply, and the rate source sums every local record they resolve, then adds the state share per the state's rateBasis.

That summing is the whole point. Local records are components, and which of them apply differs by address in a way no rule can predict:

Address Index returns Rate
701 N 7th St, Kansas City KS66101-3064 county 209 and city 36000 6.5% + 1.0% + 1.625% = 9.125%
400 Broad St, Seattle WA98109-4607 city 63000 only 6.5% + 4.05% = 10.55%

Both come out of the same code path. Taking the most specific record alone would be right for Seattle and 100 bp low for Kansas City; there is no per-state rule to encode, because the boundary file carries it.

The indexes are published: all 24 member states, 5.4 MB gzipped, mirrored to boundaries/US-XX.json.gz and refreshed quarterly, each verifiable against boundaries/manifest.json.

Three limits remain:

Boundary-index coverage is the 24 SST member states, plus two resolved by polygon and four by county.

California and New Mexico publish no boundary file but do publish an official ArcGIS service of polygons carrying the jurisdiction and its rate, which ArcGisRateSource queries by point — finer than a ZIP+4, since it is real geography rather than a postal proxy for it. Verified against both services: Los Angeles City Hall resolves 9.75%, San Francisco 8.625%, Albuquerque 7.625%, Santa Fe 8.1875%.

Florida, Pennsylvania, Hawaii and Virginia need no index at all — see below.

Because a jurisdiction carries exactly one locality, the key differs by state: a point for CA/NM, a county for FL/PA/HI/VA, and a ZIP+4 everywhere else.

Four states need only the county, and get an exact rate without a boundary file

Florida, Pennsylvania, Hawaii and Virginia are resolved exactly with no boundary index at all, because in those four nothing can tax below the county line. Resolve the county and you have the whole local share.

State Local authority Reach
FL Discretionary sales surtax, 0–2% all 67 counties
PA Allegheny 1%, Philadelphia 2% the only two that exist
HI County GET surcharge, 0.5% 4 of 4 adopters
VA Regional additions, +0.7 / +1.0 / +1.7% all 39 localities in a band

Virginia is on the list for a different reason from the other three. Its cities are not small — they are independent: under Virginia law a municipality incorporated as a city is not part of any county at all, which is why the Census counts all 38 as county-equivalents. So a Virginia city is not something sitting below a county; it is one. The 5.3% state rate already contains the mandatory statewide 1% local levy, and the 39 records are the regional additions on top, checked against the Department of Taxation's own bands.

That creates one trap worth knowing about, because Virginia has both a Fairfax County and an independent city of Fairfax — and likewise Franklin, Richmond and Roanoke. They tax different ground at different rates. The name match is ordered so the full name is tried before any shortened form, which keeps each pair apart; a pair it genuinely could not tell apart would resolve to nothing and fall back to the state rate rather than putting one authority's rate on the other's territory.

A geocoder returns the county on every US result with no add-on, so this path is not behind the rooftop opt-in: it costs nothing extra and gating it would leave those states charging the bare state share for no benefit. A Gainesville address prices at 7.5% (6% state + 1.5% county) at Confidence::Authoritative.

Two details worth knowing:

A county that levies nothing is an answer, not a gap. Citrus County's surtax is 0%, so a Citrus address prices at the 6% state rate — but Authoritative, because that IS the all-in rate there. A county that fails to MATCH resolves to the same 6% at Derived, and the confidence is the only thing telling the two apart.

Hawaii's figure is the legal rate, not the receipt rate. The GET is a tax on the seller's gross receipts, and it may itself be taxed when passed on, so a Honolulu customer sees a higher percentage than the 4.5% owed. This package computes the liability; the gross-up a seller applies to recover it is a separate calculation and is deliberately not modelled.

Most of Virginia levies no regional addition at all, and 5.3% is genuinely the whole rate there — but that is reached by the county failing to match, which is "unknown" rather than "nothing applies", so it is labelled Derived.

South Carolina looks like it belongs here and does not. 46 of its 47 local authorities are counties — but Myrtle Beach levies a 1% Tourism Development tax on top of Horry County's, and the statute lets other qualifying municipalities adopt one. Pricing from the county alone there would under-charge, so SC stays on the list below. bin/check-county-resolved.php in the dataset repo fails the build if FL, PA, HI or VA ever grows the same exception — including a Virginia town, which unlike a Virginia city does sit inside a county.

The 12 states with local tax and no rooftop path

37 states levy a local sales tax. 30 of them now have a rooftop-accurate path (the 24 SST boundary files, CA/NM by polygon, and FL/PA/HI/VA by county). That leaves twelve where nothing published resolves below the state line:

AL · AZ · CO · ID · IL · LA · MO · MS · NY · SC · TX, and AK (see below).

In eleven of those the state rate applies at Confidence::Derived — an honest floor, but a floor: Louisiana's state share is 4.45% against a combined rate that reaches 11.45%, Alabama's 4% against up to ~12.5%, Colorado's 2.9% against ~11.2%. Check $assessment->rate->confidence before relying on a US figure in one of these states, and bind a commercial adapter where the local share matters.

New York and Illinois are worth calling out for SaaS sellers specifically: both tax digital services, and both are in this list.

Colorado is still the most tractable of the twelve, but not from the outside. Its Department of Revenue runs an address-level GIS whose answers CRS 39-26-105.2 holds a relying vendor harmless for in an audit — the right foundation for an adapter. What the open web offers, though, is less than it appears (verified 2026-08-20): the public lookup redirects to a vendor-operated app at colorado.atr.avalara.com behind a terms-acceptance gate, tax.colorado.gov CloudFront-blocks every egress we have tested — datacenter and residential alike — and the API's method documentation is published only on the logged-in key-issuing screen of the SUTS Remittance Portal. The key is issued per business, so the adapter belongs host-configured — the same shape as ArcGisRateSource — and building it starts inside the host's own portal account, where both the key and the contract live. Not built yet; blocked on exactly that.

Two of the twelve have an elective escape hatch — for remote sellers, opt-in. Alabama's Simplified Sellers Use Tax (Ala. Code § 40-23-193) lets an accepted remote seller collect a flat 8% in place of the whole state+local stack, and Texas' Single Local Use Tax Rate (Tex. Tax Code § 151.0595, elected via Form 01-799) replaces the local share with a single published figure — 1.75% for 2026, redetermined annually — on top of the 6.25% state rate. The dataset carries both schemes in its additive elections section, and the engine applies one only when the host asserts the seller's own election: a state registration whose scheme is UsSalesTaxRegime::REMOTE_ELECTION_SCHEME. Asserting the scheme asserts the program's eligibility terms are met — that is the seller's fact (an accepted application, a filed form), never inferred. A supply shipped from inside the state is priced on the ordinary path (a seller with in-state presence is outside these programs), a category the state prices specially refuses rather than flattens, and an asserted election the dataset cannot price — Texas after its determination lapses, a state with no scheme — refuses loudly rather than charging either the replaced rates or an unpublished figure.

Alaska is different and now refuses outright. It levies no state sales tax while its boroughs and cities levy their own (Juneau 5%, Wrangell 7%). Alaska removed its statutory cap on local rates, so do not hard-code a ceiling. A state share of 0% there is not a conservative floor — it is an affirmative "no tax due" on a supply that is taxed — so the source returns null and the engine raises UnresolvedTaxRate rather than charging zero. Bind the ARSSTC remote-seller rate sheet to serve Alaska.

Texas is the notable near-miss: it does produce an SST-formatted address dataset, but behind an audited account portal that labels the data sensitive, so a publicly redistributable index cannot be derived from it.

A ZIP+4 or a coordinate is required, so a geocoder becomes load-bearing. Absent an add-on — or where Geocodio returns several for one address, which is refused rather than picked — no locality is attached and the state rate applies.

Address-range precision is not indexed. The boundary files carry street-level A rows (570k of Kansas' 684k), but resolving those needs a parsed street address rather than a ZIP+4, so the index carries the whole-ZIP and ZIP+4 rows only.

Absent a resolved locality the state rate applies at Confidence::Derived, which is honest about what it is; a resolved one is Confidence::Authoritative.