Handling US Sales-Tax Nexus and Registration Thresholds

US sales tax is the compliance surface that catches SaaS teams off guard, because there is no single national rate and no single registration — post-Wayfair, each state sets its own economic-nexus threshold, and once you cross it you must register and collect at a combined state-county-city rate that varies by the customer’s exact address. Miss a threshold and you accrue uncollected tax liability plus penalties; register too eagerly and you owe filings in states where you have almost no sales. This page extends VAT & GST Tax Calculation to the US regime: monitoring thresholds, timing registration, and gating collection on the tax registrations that authorize you to charge.

You reach for this once you have customers in more than a handful of states and someone asks whether you should be collecting sales tax in, say, Texas — a question that only the nexus thresholds and your rolling sales can answer.

The mental model that keeps this tractable is to separate three concerns that teams routinely tangle together: nexus (do I have an obligation in this state?), registration (am I permitted and required to collect there?), and rate (how much do I charge at this address?). Nexus is derived from your own sales history against a per-state rule. Registration is a legal status you apply for and maintain. Rate is a pure function of the destination address. Each answers a different question, each has a different owner, and a bug in one does not look like a bug in the others — which is exactly why folding all three into a single “does this customer pay tax” boolean produces defects that are painful to trace back to their source.

Trade-offs

The decision is how you track nexus and apply rates: a managed engine (Stripe Tax, Avalara) that maintains thresholds and rates for you, or a custom threshold monitor over your own sales data. The map contrasts them.

US sales-tax approaches A managed engine maintains thresholds and combined rates, while a custom monitor tracks your own sales against per-state thresholds at higher maintenance. Managed engine thresholds maintained combined rates by address the default for most SaaS Custom monitor you track per-state sales you source rate tables high maintenance, few states
Buy the rate tables and threshold tracking; the US regime's thousands of jurisdictions make a fully custom engine a maintenance sink.

For almost every SaaS, the answer is a managed tax engine for rates and thresholds, with your own lightweight monitor as a cross-check so you register before, not after, a threshold is crossed. Even on a managed engine you own the registration decision and the audit record.

Why the managed engine wins on the rate side but not the nexus side

The two halves of this problem have very different maintenance profiles, and conflating them leads teams to either over-build or under-monitor. Rate resolution is a data problem at a scale no single company should own: there are roughly 13,000 taxing jurisdictions in the United States, rates change hundreds of times a year, and the boundary between one city’s special-district rate and the next does not follow ZIP codes. A ZIP-centroid lookup is wrong often enough — a ZIP can straddle two counties and three cities — that serious engines geocode the full street address to a rooftop and resolve the jurisdiction stack from that point. Reproducing that in-house means licensing boundary data and rate tables anyway, so you have bought the hard part and still carry the integration. That is why the rate side is a clear buy.

Nexus monitoring is the opposite: it is small, it is specific to your own sales ledger, and getting it wrong is a compliance event rather than a few cents of rounding. A managed engine will happily tell you a customer at a given address owes an 8.25% combined rate, but it does not know whether you have decided to register in that state, whether your registration is effective yet, or how your finance team defines a taxable sale versus an exempt one. Keep that logic where you can read it, test it, and point an auditor at it. The pattern that scales is: engine for rates, your own nexus_thresholds and tax_registrations tables for the decision to collect.

The real cost is filings, not the engine fee

The line item people compare is the engine’s per-transaction or percentage-of-tax fee, but that is rarely what makes US sales tax expensive. The cost that grows with your footprint is filing: once registered in a state you owe a return every period — monthly, quarterly, or annually depending on volume — even in months with zero taxable sales, and each missed zero-dollar return can carry its own penalty. Registering in a state where you have three customers and $4,000 of annual revenue buys you twelve or four returns a year in perpetuity for very little collected tax. This asymmetry is why “register everywhere to be safe” is bad advice: every registration is a standing obligation, not a one-time checkbox. The monitor exists precisely so you register when the law requires it and not a state sooner, keeping the filing calendar as small as the rules allow.

What the engine cannot decide for you

Three judgment calls stay on your side of the line no matter which vendor you pick. First, product taxability: SaaS is taxed as a service in some states, as tangible personal property in others, and is exempt in a handful, and the classification of your specific offering (is it prewritten software delivered electronically, or a nontaxable data-processing service?) is a position your tax advisor takes, not a lookup. That position is not global either: the same SaaS product can be taxable in one state and exempt in the next, so taxability is itself a per-state, per-product mapping you store and version, not a single flag on the plan. When the advisor changes a position — a state issues new guidance, or you reclassify a feature — you want the change dated so invoices before it keep their old treatment and only new invoice_id rows pick up the revised rule. Second, the registration trigger — the engine reports sales, but the choice to register the moment you cross versus after a grace period, and whether to register voluntarily in a state where you have physical presence but low sales, is a business decision with filing-cost consequences. Third, marketplace-facilitator handling: if a portion of your revenue flows through a marketplace that already collects and remits, those transactions may still count toward the threshold in some states even though you do not collect on them, so your monitor and the engine must agree on what is in the denominator.

Step-by-Step Implementation

The five steps model per-state thresholds, aggregate rolling sales, alert on approach, gate collection on registration, and apply the destination rate. The threshold-monitor diagram shows how a rolling sales total crossing a state line triggers registration.

Nexus threshold monitor Rolling per-state taxable sales are compared to the state's revenue and transaction thresholds; approaching the line triggers a registration alert before collection is required. Rolling sales per state, 12-mo vs threshold $100k / 200 txns 80%: alert register in time crossed: collect after registration
Alert at 80% of a threshold so registration completes before collection is legally required — crossing then flips collection on.

1. Model per-state thresholds

Each state has a revenue threshold and, in many states, a transaction-count threshold; crossing either establishes nexus.

CREATE TABLE nexus_thresholds (
  state_code       CHAR(2) PRIMARY KEY,
  revenue_threshold_minor BIGINT NOT NULL,   -- e.g. 10000000 = $100,000
  transaction_threshold   INT,               -- e.g. 200; NULL if none
  measurement_period      TEXT NOT NULL       -- 'prior_or_current_calendar_year'
);

The subtle field is measurement_period. States disagree on what window you measure against, and the disagreement is not cosmetic — it decides whether a sale from eleven months ago still counts. Some states use the current or prior calendar year (if you crossed last year, you have nexus all of this year); others use a rolling trailing-twelve-month window that recomputes every day; a few use their own fiscal year. Encoding this as free text and branching on it in the aggregation query is error-prone, so most teams normalize it to an enum plus an anchor date and let a single window function compute the bound. The revenue threshold is stored in minor units to avoid float drift: 10000000 means exactly $100,000.00, and the comparison is integer-to-integer. Storing it as a decimal dollar amount invites the classic 99999.99 < 100000 off-by-a-cent bug at exactly the boundary you most need to get right.

2. Aggregate rolling sales and alert on approach

Sum taxable sales and transaction counts per state over the measurement window, and alert at 80% so registration has lead time.

SELECT ship_to_state,
       SUM(amount_minor) AS revenue_minor,
       COUNT(*)          AS txn_count
FROM taxable_sales
WHERE sold_at >= date_trunc('year', now())
GROUP BY ship_to_state;
-- compare each row to nexus_thresholds; alert when >= 80% of either threshold

The 80% alert threshold is not arbitrary; it is a lead-time budget. Registration is not instant — most states issue a permit within a few business days, but several take two to four weeks, and a handful require you to name an effective date and back-file if you got it wrong. If you only alert at 100%, you are already collecting-eligible before the permit arrives, and every taxable sale in that gap is uncollected liability you eat out of margin. Alerting at 80% gives finance a runway to file, name a forward-dated effective date, and update tax_registrations before the crossing. For a state you are approaching quickly — say a new enterprise customer_id lands three six-figure invoice_id orders in a month — you may want a second, steeper alert at 95% that pages rather than emails, because the linear-extrapolation assumption behind an 80% warning breaks when a single large order can vault you over in one billing cycle.

One aggregation trap: the taxable_sales feed must exclude the transactions the marketplace facilitator already remitted, or must flag them, depending on the state’s rule. If you count marketplace sales as revenue for the rate but not for the threshold in a state that says they count for the threshold, your monitor silently under-reports how close you are. Keep a channel column on each row and make the threshold query’s inclusion rule explicit per state rather than global.

3. Gate collection on an active registration

Charging tax you are not registered to remit is itself a violation. The tax_registrations table authorizes collection per state.

def should_collect(state: str, registrations: set[str]) -> bool:
    return state in registrations   # ✅ only collect where registered

The set[str] here is a simplification for the example; a production gate keys on more than the two-letter code. A registration has an effective date, and a sale dated before that date must not be taxed even though the state now appears in the set, so the real predicate is “is there a registration for this state whose effective date is on or before the sale date and whose status is active.” Registrations also get revoked or lapse for non-filing, which is why the gate reads current state rather than a cached boolean stamped on the subscription_id at signup. Treating the registration set as immutable is how teams keep charging tax in a state they quietly deregistered from — money they now hold and must either remit or refund, both of which are worse than never having collected it.

There is a symmetric failure worth a test of its own: collecting nothing in a state where you are registered and have nexus, because the gate short-circuited on a stale registration cache after a deploy. Both directions of the gate — false positive and false negative — are compliance events, so the function that answers should_collect should be pure, take the registration snapshot as an argument (as above) rather than reaching for global state, and be exercised at the exact effective-date boundary.

4. Apply the combined destination rate

US sales tax is destination-based: the rate is the sum of state, county, city, and special-district rates for the customer’s exact address, not a flat state rate.

def combined_rate_bps(address, rate_service) -> int:
    r = rate_service.lookup(address)   # resolves to the exact jurisdiction stack
    return r.state_bps + r.county_bps + r.city_bps + r.special_bps

Working in basis points keeps the rate an integer through the whole calculation. A combined rate of 8.25% is 825 bps, and tax on a 2000 minor-unit ($20.00) line is 2000 * 825 // 10000 = 165 minor units, computed with integer arithmetic and a documented rounding rule rather than a float multiply that can land on 164.999999. The rounding rule matters more than it looks: some jurisdictions require rounding per line item, others on the invoice total, and the two disagree by a cent on multi-line invoices often enough that an auditor will notice. Pick one, apply it consistently, and store the rate you used on the invoice_id so a later rate change never silently rewrites history.

The registration decision is a workflow, not a flag

Crossing a threshold does not automatically make you compliant; it starts a clock. The workflow between “monitor fired at 80%” and “gate flips to collect” has real steps: a human confirms the crossing is genuine (not a data-quality blip from a mis-mapped ship_to_state), finance files the registration application, the state issues a permit with an effective date, someone writes that date and the permit number into tax_registrations, and only then does should_collect return true for new sales. Model this as an explicit state machine — approaching → registering → active → (lapsed) — rather than a lone boolean, because each transition has an owner and an audit artifact, and an auditor three years later will ask for exactly that trail. The nastiest gap is between crossing and the effective date: sales in that window are legally due, and depending on the state you either back-file and remit them out of margin or, better, timed your registration so the effective date preceded the crossing.

Verification & Testing

The tests prove the threshold logic, the registration gate, and the combined-rate resolution. Assert that crossing either the revenue or transaction threshold flips nexus true. Assert that a state without an active registration collects zero tax even past nexus (registration gates collection). Assert two addresses in the same state but different cities get different combined rates. The panel lists them.

Boundary cases that catch real bugs

Threshold logic lives and dies at the boundary, so test the exact-equality case explicitly: a state whose rule is “meets or exceeds $100,000” must flip nexus true at precisely 10000000 minor units, not at 10000001. Off-by-one here is not academic — it is the difference between registering on time and registering a month late. Test the transaction-count path in isolation with a fixture that sits far below the revenue line but crosses on count: 210 orders of $50 each is 1050000 minor units, nowhere near a revenue threshold, yet it establishes nexus in a state with a 200-transaction rule. A test suite that only exercises the revenue path will pass forever and still let a low-price, high-volume product accrue silent liability.

Test the measurement window with sales dated at the edges of it. A trailing-twelve-month window evaluated today should include a sale from 364 days ago and exclude one from 366 days ago; a calendar-year rule should include January 1 of the current year and, for the “prior or current year” variant, still show nexus if last year crossed even when this year is quiet. Freeze the clock in these tests — a window computed from now() is non-deterministic and will flake exactly once a day at the boundary.

Testing the gate at the effective-date edge

The registration gate needs two boundary tests that a naive state in registrations check would never surface. First, a sale dated one day before the registration’s effective date must collect zero even though the state is registered — pre-registration sales are not retroactively taxable through the gate. Second, a sale after a registration lapses must also collect zero, proving the gate reads live status rather than a value cached against the customer_id. Round the trip out with an idempotency test: replaying the same taxable event with the same idempotency_key must not double-count it toward the threshold, or a webhook retry can push a state over the line on paper when real sales have not.

Finally, snapshot the combined rate onto the invoice and assert it is stable. Look up the rate for an address, persist it on the invoice_id, then change the rate table and re-render the invoice; the stored rate must not move. Historical invoices are legal records, and a rate table that back-propagates turns a filed return into a discrepancy.

One more test earns its keep at reconciliation time: the tax you collected per state per period must equal the tax your engine says you owe for that same period, within the rounding rule you chose. Feed a period’s invoices through both paths and assert equality. A drift here — usually from a rate that changed mid-period, a late refund, or a per-line versus per-invoice rounding mismatch — is what turns an otherwise clean return into an amended one, so catching it in a test beats catching it in an audit.

US sales-tax tests Either threshold establishes nexus, an unregistered state collects nothing, same-state different-city rates differ, and the alert fires at 80 percent. Either threshold revenue or count nexus true Gate unregistered collect zero Combined rate same state diff city, diff rate Alert at 80% lead time
The registration gate test is the compliance one — collecting where you are not registered is itself a violation.

Gotchas & Production Pitfalls

The pitfalls are per-state (not national) thresholds, transaction-count nexus, flat-state-rate assumptions, and collecting before registering. The map groups them.

US sales-tax pitfalls Treating thresholds as national, ignoring transaction-count nexus, using a flat state rate, and collecting before registering are the recurring pitfalls. National one threshold → per-state Count nexus revenue only → track txn count Flat rate state only → combined by address Collect early before register → gate on registration
Four pitfalls — treating US tax as one national rate is the assumption that produces the biggest liability surprise.
  • Assuming a national threshold. There is none. Each state sets its own revenue and transaction thresholds; track nexus per state.
  • Ignoring transaction-count nexus. Many states establish nexus at 200 transactions regardless of revenue. A low-price, high-volume product can cross on count long before revenue.
  • Using a flat state rate. The taxable rate is the sum of state, county, city, and special-district rates for the destination address. A flat state rate under-collects in most cities.
  • Collecting before registering. Charging tax you are not registered to remit is a violation in its own right. Gate collection on an active tax_registrations row per state.

The pitfalls the map does not show

Beyond the four headline traps, a few slower failures accumulate quietly. Address quality upstream of the rate lookup is the one that scales badly: a rate service can only be as precise as the address you hand it, and if checkout collects a ZIP with no street line, the engine falls back to a ZIP centroid and may pick the wrong city rate. The under- or over-collection is small per transaction but systematic, and it compounds across every invoice for that customer_id. Validate and, where possible, geocode the address at the point of sale, and store the resolved jurisdiction alongside the rate so a later dispute is auditable.

Ignoring trailing-nexus obligations after you fall below a threshold is another. Crossing a threshold usually obligates you for the current and often the following period even if sales then drop; you do not get to deregister the instant you dip under, and some states require a formal closing return. Wiring the gate to flip off automatically the moment rolling sales fall below the line is how teams stop collecting while still legally on the hook. Deregistration is a deliberate workflow, mirror-imaging registration, not a side effect of the monitor.

Exempt customers and exemption certificates round out the list. A registered reseller or a tax-exempt nonprofit should not be charged, but only if you hold a valid, unexpired exemption certificate on file for that customer_id. Collecting tax from a genuinely exempt buyer is a refund you now owe; failing to collect without a certificate on file is liability you eat. The gate therefore has a second dimension beyond registration: even where you are registered and past nexus, a verified certificate flips collection off for that specific buyer, and the certificate’s expiry needs its own monitoring so it does not silently lapse.

Frequently Asked Questions

What triggers an obligation in a state? Physical presence, or economic activity above that state’s threshold, measured in revenue or transaction count over a defined lookback. The thresholds differ by state and change.

How should thresholds be monitored? Continuously, against a rolling window, with an alert well before the threshold is reached. Discovering the crossing after the fact means back taxes and penalties rather than a registration.

Is SaaS taxable everywhere? No — treatment varies by state and sometimes by how the service is delivered or characterised. This is precisely why a product tax code, rather than a single rate, drives the calculation.

What happens after registration? Filing obligations begin, usually periodic and per state, including for periods with no sales. Missing a zero return is a common and entirely avoidable penalty.