Overview
Tax is the part of commerce most likely to differ from what you assumed. A US shopper expects$17.99 on the shelf and a little more at the till. A German shopper expects €17.99 to be the whole story, VAT already inside it. Both are correct, and a store selling to both has to do both.
Spree handles this by keeping three things separate:
- What is being sold — a tax category groups products taxed the same way
- Where the customer is — their market and shipping address
- Who works out the number — Spree’s own rates, or an external tax service
Tax categories
A tax category is how you say “these things are taxed alike”. Children’s clothing, books and hot food are treated differently from general goods in many countries, and a tax category is where that distinction lives. Every product carries one. If you don’t choose, the default is used.tax_code matters once you connect an external service — it is how you tell that service what kind of thing this is, in the vocabulary it already uses.
Tax rates
A tax rate is the number itself, plus where it applies. Whether it is already in the price is the market’s choice, not the rate’s.DE, US + CA. There’s no separate region record to create first.
Included or added on
A market’stax_inclusive setting is the single most consequential setting here, so it’s worth being precise about what it does.
- Added on (US style)
- Included (EU style)
The shelf price is what the product costs. Tax is worked out at checkout, once the address is known, and added.A $17.99 item at 5%:
Set
tax_inclusive: false on the market.Tax lines
Whatever works out the tax, the record is the same: a tax line per charge, saying what was taxed and how.Tax given back
When a customer returns something, the tax they paid on it goes back with the money. That tax is recorded as tax lines too, markedcredit, on the return, claim or exchange line rather than the order line. Each one names the sale’s tax line it gives back (original_tax_line_id, Admin API), keeps that line’s rate and label, and covers only the units that came back. They never change the order’s own tax totals, and the order’s tax lines list above shows only what was charged.
When tax is worked out
Tax is recalculated whenever the answer could have changed — an item added, an address entered, a delivery option chosen. Every one of those requests returns the updated cart, so a storefront never needs a separate “refresh the tax” call.1
Before an address is known
Prices are shown using the market’s tax treatment. For an inclusive market the displayed price is the real one; for an added-on market tax simply hasn’t been worked out yet.
2
Once the shipping address is entered
The address decides the real rate. Tax lines are written against each item, the delivery charge, and any taxable fee.
3
At completion
Whatever the tax is at that moment is what’s charged, and the order keeps it. A rate change next month does not rewrite an order from last month.
Connecting a tax service
Spree’s built-in rate tables are fine when the rules are simple — one country, or a handful of clearly-defined rates. They become a liability where the rules are not: US sales tax varies by city and changes constantly, and cross-border EU VAT has thresholds that shift with your turnover. For those, connect a real tax service. The provider is chosen per Market, which means one store can use its own rates in a simple market and an external service in a complicated one.estimate and estimate_replacement receive it as tax_inclusive:. A provider passes it on to its service, such as a “tax included” flag on each line, and marks its tax lines included to match.
The provider also decides the tax given back on returns, claims and exchanges, and the refund pays exactly what it answers. Spree’s own rates give back each returned unit’s share of the tax the sale actually charged, so the customer gets back what they paid even if a rate has been edited since. An external service may calculate it itself instead, for example at the rates in force on the day of the sale. If it cannot be reached, the return is refused until it can, rather than refunding tax nobody worked out.
If you are writing a provider, these are the calls Spree makes after the sale:
estimate_refund and estimate_replacement have no default — a provider without them cannot open a return, claim or exchange. To give back exactly what the sale charged, as Spree’s own rates do, answer estimate_refund with Spree::TaxProvider::RecordedShare:
server/app/models/my_app/tax_provider.rb
refund and commit_replacement are optional: by default they do nothing, which suits a service that keeps no record of sales.
Tax-exempt customers
Business customers are often exempt, and the paperwork is real. Spree records a customer’s or company’s tax identifier — a VAT number, an ABN — and can validate it. Companies can also hold exemption certificates, scoped to the country or state that issued them. Exemption is decided when tax is worked out, not stored as a flag on the customer, so an expired certificate stops applying by itself. See Companies. Each certificate points at one of the store’s tax exemption reasons — a list the merchant manages, like return and refund reasons. The tax service receives the reason’scode. A tax service that decides exemptions by code declares the codes it understands, and the dashboard offers them when a merchant adds a reason. See Exemption reasons.
Related
- Order totals — how tax rolls into what a customer pays
- Markets — where tax treatment and provider are chosen
- Returns, exchanges and claims — how tax goes back with a refund
- Addresses — what determines the rate
- Companies — B2B tax identifiers and exemptions

