I just open-sourced a WooCommerce plugin that replaces TaxJar / Avalara / WooCommerce Tax (Jetpack) with a free, self-hostable US sales-tax engine. Here's the pitch and how to install it.
The problem
If you run a US-shipping WooCommerce store, your options for accurate destination-based sales tax are roughly:
| Service | Pricing |
|---|---|
| WooCommerce Tax (Jetpack) | Free up to 200 transactions/month, paid above |
| TaxJar | From $19/mo + transaction fees |
| Avalara AvaTax | Enterprise pricing (usually starts in the thousands/yr) |
| Stripe Tax | 0.5% per transaction |
| Manual rate tables | Free but you maintain ~50 jurisdictions' worth of rules |
Above ~200 transactions/month or ~$50K/yr in revenue, the math gets uncomfortable fast. At $1M ARR, Stripe Tax alone costs ~$5K/yr — and that's just the Stripe portion.
The fix
Self-host the math. OpenSalesTax is an open-source US sales-tax calculation engine — Docker container, REST API, ~52 jurisdictions, real per-state taxability rules. OpenSalesTax for WooCommerce is the plugin that wires it into WC's checkout flow.
Both are Apache 2.0:
- Engine: https://github.com/ejosterberg/open-sales-tax
- WC plugin: https://github.com/ejosterberg/opensalestax-woocommerce
Setup (~30 min)
# 1. Stand up the engine (5 min)
git clone https://github.com/ejosterberg/open-sales-tax
cd open-sales-tax
docker compose up -d
curl http://localhost:8080/v1/health
# → {"status":"ok","version":"0.39.0","database_connected":true}
# 2. Install the plugin
composer require ejosterberg/opensalestax-woocommerce
# (or git clone into wp-content/plugins/ and run composer install --no-dev)
# 3. Activate + configure
wp plugin activate opensalestax-woocommerce
wp option update opensalestax_base_url "http://your-engine:8080"
wp opensalestax test-connection
# → Success: Engine v0.39.0 is ok — DB up
That's it. The next cart that ships to a US address gets destination-based sales tax computed via the engine.
Verifying it works
Drop a $100 product into a cart with a Minneapolis MN shipping address (ZIP 55401):
Subtotal: $100.00
Total tax: $9.03
Tax lines (1):
- code=US-OPENSALESTAX-1, label=OpenSalesTax, amount=$9.03
Open the order in the admin and you'll see the full per-jurisdiction breakdown:
| Type | Jurisdiction | Rate | Tax |
|---|---|---|---|
| state | Minnesota | 6.875% | $6.88 |
| county | Hennepin County | 0.150% | $0.15 |
| city | Minneapolis | 0.500% | $0.50 |
| district | Hennepin County Transit Sales Tax | 0.500% | $0.50 |
| district | Metro Area Transportation Sales Tax | 0.750% | $0.75 |
| district | Metro Area Sales and Use Tax for Housing | 0.250% | $0.25 |
| 9.025% | $9.03 |
That's the correct combined rate for Minneapolis. TaxJar / Avalara show one flat number; OpenSalesTax shows you exactly where every penny went — useful for audit reconciliation.
What's in the box (v0.4.0)
Core (v0.1):
-
woocommerce_calc_taxfilter integration (works with classic checkout AND Cart/Checkout Blocks via the Store API) - Settings page under WC → Settings → Tax → OpenSalesTax
- "Test connection" button
- Tax-exempt customers honored via
WC()->customer->is_vat_exempt() - WP transient caching (default 60-min TTL, configurable)
- Configurable error fallback:
block(no tax line) orzero($0 + log)
v0.2 — Tax classes: Custom WC tax-class → OST category mapping. Map clothing, groceries, or any custom class to the right OST category (or mark non-taxable). Built-in defaults still apply for standard / reduced-rate / zero-rate. Configure via the admin UI (v0.3.3) or WP-CLI.
v0.3 — Operational visibility:
- Per-order jurisdiction breakdown — every order stores the engine's full state/county/city/district split as meta and renders the table above on the WC admin order-edit page
- Status dashboard widget — WP-admin home page shows engine reachability, version, and today's order count with breakdown captured. 60s transient cache so it never hammers the engine
- Recent-calculations debug log — opt-in 50-entry ring buffer captures every cart calc (cache-hit / engine-call / error) with timing, ZIP, category, amount, tax. Useful for "why is this rate $8.25 when it should be $9.03?" debugging
- Admin-UI tax-class mapper — replaces the CLI-only configuration with a UI right in the settings page
v0.4 — WooCommerce Subscriptions: If you run WC Subscriptions, every renewal order gets a fresh tax recalculation against today's rates instead of inheriting the parent sub's stale tax line. State law changes mid-subscription? You stay accurate. (Auto-detected; no-op if WC Subscriptions isn't installed.)
Quality bar: PHPStan level=max clean, 98 unit tests + integration test on a real WP+WooCom install, security review (docs/SECURITY-REVIEW.md), Apache 2.0, DCO sign-off.
What's intentionally NOT in scope
- Tax filing / remittance — calculation only, by design. The merchant remits.
- Multi-currency carts — engine is USD-only; non-USD throws.
- Stripe Connect / multi-vendor marketplace tax allocation — not yet.
- Per-product custom tax-code overrides — use the tax-class mapper for category-level distinctions.
Honest disclaimer
Tax calculations are provided as-is. The merchant is solely responsible for tax-collection accuracy and remittance to the appropriate jurisdictions. Verify against your state Department of Revenue before remitting. (This isn't lawyer-speak — it's the genuine constraint of any sales-tax integration; OpenSalesTax computes the math, you remit.)
Compare
| Cost | Self-hosted | Per-jurisdiction breakdown | WC Subs auto-recalc | |
|---|---|---|---|---|
| Stripe Tax | 0.5%/txn | ❌ | ✅ | ✅ |
| TaxJar API | from ~$0.50/txn | ❌ | ✅ | partial |
| Avalara AvaTax | enterprise | ❌ | ✅ | partial |
| WC Tax (Jetpack) | $0 / paid above | ❌ | ❌ | ❌ |
| OpenSalesTax + this plugin | $0 software | ✅ | ✅ | ✅ |
Try it
If you run a US-shipping WooCommerce store and you're hitting the WC Tax (Jetpack) 200-transaction limit OR paying TaxJar, give it a try. File issues at https://github.com/ejosterberg/opensalestax-woocommerce/issues — the plugin needs real-merchant feedback on:
- Address scenarios where customers ship to less-common ZIPs (rural / overseas-territory edges)
- WC Subscriptions integration — verify the renewal recalc against your real subscription flow
- Anywhere the per-jurisdiction breakdown disagrees with what your state DOR expects
composer require ejosterberg/opensalestax-woocommerce to get started.
Top comments (0)