@ejosterberg/saleor-app-opensalestax
v1.2.0
Published
Saleor Tax App connector — destination-based US sales tax via the self-hosted OpenSalesTax engine
Maintainers
Readme
opensalestax-saleor
Saleor Tax App connector — destination-based US sales tax via the self-hosted OpenSalesTax engine. No per-transaction fees. No SaaS lock-in. Dual-licensed Apache-2.0 OR GPL-2.0-or-later.
What this is
A Saleor Tax App
that routes Saleor's CHECKOUT_CALCULATE_TAXES and
ORDER_CALCULATE_TAXES webhooks through your own OpenSalesTax
engine and returns the destination-based US tax breakdown back to
Saleor.
You run both the Saleor Tax App and the OpenSalesTax engine on your own infrastructure. Saleor calls the app over HTTPS; the app calls the engine over your private network.
Quickstart (≤10 minutes)
Prerequisites
- A running Saleor instance, v3.20 or later, with the Tax App framework
- Docker + Docker Compose
- A network path from Saleor → this app (HTTPS in production)
Steps
Clone and configure
git clone https://github.com/ejosterberg/opensalestax-saleor.git cd opensalestax-saleor cp .env.example .env # Edit .env: set APP_API_BASE_URL to the public URL Saleor will reachBoot the stack
docker compose up -dThree containers start: the Saleor Tax App, the OpenSalesTax engine, and a Postgres for the engine.
The shipped
docker-compose.ymlpulls a multi-arch prebuilt image from GitHub Container Registry (ghcr.io/ejosterberg/opensalestax-saleor:latest) that's signed with SLSA provenance attesting it was built from this repo's tagged release commit. If you'd rather audit the source and build locally, swap theimage:line forbuild: .indocker-compose.yml.Verify the app is healthy
curl http://localhost:3000/health # → {"ok": true, "version": "...", "rtt_ms": ...} curl http://localhost:3000/api/manifest | jq .name # → "OpenSalesTax"Install into Saleor In the Saleor Dashboard → Apps → Install external app, paste
${APP_API_BASE_URL}/api/manifest. Review the requested permission (HANDLE_TAXES) and confirm.Capture the APL token The app's stdout (
docker compose logs opensalestax) prints theSALEOR_API_URL,SALEOR_APP_ID, andSALEOR_APP_TOKENSaleor delivered. Copy those into.env, thendocker compose up -dto make them survive restarts.Set the app as your channel's tax provider In the Saleor Dashboard, go to your USD channel → Configuration → Taxes, set Tax calculation strategy to
TAX_APP, and pick OpenSalesTax as the tax app.Run a test checkout Create a checkout against a US ship-to address with a non-zero shipping cost. Saleor calls the app, the app calls the engine, and the response carries per-line + per-jurisdiction tax.
That's it.
How it works
┌────────────┐ POST /api/webhooks/... ┌──────────────────────┐
│ Saleor │ ─────────────────────────► │ opensalestax-saleor │
│ (v3.20+) │ │ (this Tax App) │
└──────┬─────┘ └────────────┬─────────┘
│ tax response (JSON) │
│ ◄────────────────────────────────────────────│
│ │ POST /v1/calculate
│ ▼
│ ┌─────────────────────────────┐
│ │ OpenSalesTax engine │
│ │ (your self-hosted copy) │
│ └─────────────────────────────┘For each CHECKOUT_CALCULATE_TAXES or ORDER_CALCULATE_TAXES
event, the app:
- Verifies the Saleor JWT (delegated to
@saleor/app-sdk) - Gates: requires
currency=USD,country=US, valid ZIP. If any gate fails, returns an empty response so Saleor falls back to its catalog rates. - Transforms the Saleor payload into the OST engine's
POST /v1/calculaterequest. - Calls the engine; on success transforms the response into Saleor's expected per-line tax breakdown.
- On engine error (fail-soft, default): returns empty + logs a warning. Saleor falls back to its catalog rates and the checkout proceeds.
Configuration
All configuration is via environment variables. See
.env.example for the full list.
| Variable | Required | Default | Notes |
|---|---|---|---|
| APP_API_BASE_URL | ✅ | — | Public URL Saleor will call. Must be http(s). |
| OSTAX_API_URL | ✅ | — | OpenSalesTax engine base URL. |
| PORT | | 3000 | HTTP port the app listens on. |
| OSTAX_API_KEY | | — | Sent as X-API-Key if set. |
| OSTAX_TIMEOUT_MS | | 5000 | Per-engine-request timeout. |
| OSTAX_FAIL_HARD | | unset | "1" opts into fail-hard mode. |
| OSTAX_NEXUS_STATES | | unset | Comma-separated list of US 2-letter state codes (e.g. "MN,WI,IA") for the per-state nexus filter (see below). Unset / empty = filter disabled (engine called for every cart, pre-v1.2 behavior). |
| SALEOR_API_URL | | — | APL seed (written by install). |
| SALEOR_APP_ID | | — | APL seed. |
| SALEOR_APP_TOKEN | | — | APL seed. Treat as a secret. |
Per-state nexus filter (CP-3, v1.2.0)
Most US merchants only have nexus (sales-tax-collection obligation) in a small set of states — typically 1–3. Without a filter, every cart goes to the engine even when the merchant has no obligation to collect for that destination.
Set OSTAX_NEXUS_STATES to a comma-separated list of US 2-letter
state codes to restrict engine round-trips to ship-tos in those
states:
OSTAX_NEXUS_STATES=MN,WI,IAWhen the filter is active:
- Carts whose ship-to state IS in the list → engine called as usual.
- Carts whose ship-to state is NOT in the list → short-circuit with an empty tax response. No engine RTT. Saleor falls back to its catalog rates (typically zero).
- Carts whose ship-to state is missing/unresolvable AND the filter is active → fail-closed (also short-circuit). The safer default for a merchant who explicitly opted into the filter.
When OSTAX_NEXUS_STATES is unset or empty, the filter is disabled
and every cart calls the engine (pre-v1.2 behavior — fully backward
compatible).
Brings this connector in line with WooCommerce v0.5, Vendure v1.2, and Odoo v0.3, which already shipped this filter.
What it does NOT do
- ❌ File tax returns
- ❌ Remit collected tax
- ❌ Validate addresses (call USPS / Smarty separately)
- ❌ Non-USD currency or non-US destinations (returns empty response; Saleor falls back to catalog rates)
- ❌ Marketplace-facilitator handling for NJ / CA seller-of-record edge cases (manual)
This is a calculation connector. Filing and remittance stay with the merchant.
Troubleshooting
Install fails — "could not reach manifest URL"
Saleor must be able to reach APP_API_BASE_URL/api/manifest
from its own network. If Saleor runs in Docker on the same
host, use the docker-compose service name or the host's LAN IP
— localhost won't resolve to your machine from inside Saleor's container.
Checkout hangs, then proceeds with zero tax
The app's fail-soft default returns empty tax on engine errors.
Check docker compose logs opensalestax for engine_error
entries. Common causes:
OSTAX_API_URLnot reachable from inside the app's container- OST engine database down or migrating
- Engine returned 5xx (see the engine's own logs)
To make this loud instead of silent, set OSTAX_FAIL_HARD=1.
Saleor will then surface the engine error to the checkout flow,
blocking the order until the engine recovers.
"Signature verification failed" in app logs
The SALEOR_APP_TOKEN in your env doesn't match the token
Saleor stored on install. Re-install the app from the Saleor
Dashboard, capture the new token from app stdout, and update
.env.
Tax returns zero for what should be a taxed US cart
- Confirm
currencyisUSDandaddress.country.codeisUSon the checkout. The app intentionally returns empty for non-USD / non-US (see "What it does NOT do"). - Confirm
address.postalCodematches^\d{5}(-\d{4})?$.12345,12345-6789accept;K1A 0B1rejects. - Confirm the OST engine has rate data loaded for the destination state. The engine's own admin tells you what's missing.
Compatibility
| Component | Tested with |
|---|---|
| Saleor | v3.20+ (GA Tax App framework) |
| OpenSalesTax engine | v0.55.4 (HTTP API v1) |
| Node | v20 LTS (tested 20 / 22 / 24 in CI) |
| Container image | linux/amd64, linux/arm64 |
The OST engine HTTP API is pinned to v1; the engine maintains backwards compatibility within v1. Cross-major engine bumps will land in a new minor of this connector.
Installing as a library
The connector is also published as an NPM package for programmatic consumers (custom Saleor app distributions, monorepos):
npm install @ejosterberg/saleor-app-opensalestaxMost merchants don't need this — git clone + docker compose up
is the supported install path. The NPM package is published with
provenance attestation
so consumers can verify each release was built from this exact
repo by GitHub Actions.
Development
npm ci
npm run lint
npm run typecheck
npm test # 55 tests, ~12s
npm run check # lint + typecheck + test + audit (the merge gate)The app boots locally with npm run dev. Tests against the
in-tree OST engine container are gated on the OSTAX_API_URL
env var — set it to the engine URL to enable the live integration
test.
Contributing
See CONTRIBUTING.md. DCO sign-off (git
commit -s) is required on every commit. No AI co-author trailers.
License
Dual-licensed under your choice of Apache-2.0 OR GPL-2.0-or-later. See LICENSE.
Related projects
| Project | Stack | Status | |---|---|---| | opensalestax | OST engine (Python) | shipped | | opensalestax-python | Python SDK | shipped (PyPI) | | opensalestax-medusa | Medusa v2 plugin | shipped (NPM) | | opensalestax-woocommerce | WordPress plugin | shipped | | opensalestax-odoo-src | Odoo connector | shipped (PyPI) | | opensalestax-saleor | Saleor Tax App | this repo |
