PHPackages                             ejosterberg/opensalestax-stripe-php - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. ejosterberg/opensalestax-stripe-php

ActiveLibrary[Payment Processing](/categories/payments)

ejosterberg/opensalestax-stripe-php
===================================

Stripe Tax replacement using OpenSalesTax — server-side calculation library for PHP SaaS.

v0.2.0(3w ago)06[2 PRs](https://github.com/ejosterberg/opensalestax-stripe-php/pulls)Apache-2.0PHPPHP &gt;=8.2CI passing

Since May 4Pushed 3w agoCompare

[ Source](https://github.com/ejosterberg/opensalestax-stripe-php)[ Packagist](https://packagist.org/packages/ejosterberg/opensalestax-stripe-php)[ Docs](https://github.com/ejosterberg/opensalestax-stripe-php)[ RSS](/packages/ejosterberg-opensalestax-stripe-php/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (5)Versions (8)Used By (0)

opensalestax-stripe-php
=======================

[](#opensalestax-stripe-php)

> Replace Stripe Tax with self-hosted [OpenSalesTax](https://github.com/ejosterberg/open-sales-tax). Server-side US sales tax calculation library for PHP SaaS using Stripe.

[![License](https://camo.githubusercontent.com/adf8a615e9d793037f69f64605238b3cfdf5ad323b7064fc5c6e18a597850450/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d417061636865253230322e302532304f5225323047504c253230322e302d2d6f722d2d6c617465722d626c7565)](LICENSE) [![PHP](https://camo.githubusercontent.com/184ea983600c41146ee84f2391d4841a638528091c0262f5e51738e676897fad/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d254532253839254135382e322d373737626234)](composer.json)

**Status:** v0.1 alpha. Tested against `stripe/stripe-php` v20 + OpenSalesTax engine v0.24.

What this saves you
-------------------

[](#what-this-saves-you)

Stripe Tax charges **0.5% per transaction** (or $0.50, whichever is greater). At scale, that's real money:

Annual revenueStripe Tax annual costThis connector + self-hosted engine$50K MRR / $600K ARR~$3,000/yr$0 software cost$100K MRR / $1.2M ARR~$6,000/yr$0$250K MRR / $3M ARR~$15,000/yr$0$500K MRR / $6M ARR~$30,000/yr$0You still pay infrastructure: a small VM running the OpenSalesTax engine (~$20–50/mo) plus a few hours/year to deploy engine updates. Below ~$1M ARR, Stripe Tax is reasonable. Above that, the math flips fast.

How it works
------------

[](#how-it-works)

This connector reads a Stripe `Invoice` or `Checkout\Session`, calls your OpenSalesTax engine for the right ZIP-based tax, and returns a structured breakdown you apply back to the invoice via `Stripe\TaxRate`. Three-line core path:

```
$breakdown = InvoiceCalculator::calculateForInvoice($invoice, $ostClient);
$taxRate   = \Stripe\TaxRate::create([...$breakdown->combinedRatePct... ]);
\Stripe\Invoice::update($invoice->id, ['default_tax_rates' => [$taxRate->id]]);
```

That's the entire integration shape. Full code below.

Install
-------

[](#install)

```
composer require ejosterberg/opensalestax-stripe-php
```

This pulls in:

- `ejosterberg/opensalestax` — the OST PHP SDK (calls your engine)
- `stripe/stripe-php` ^20.0 — the official Stripe SDK (you almost certainly have this already)

You'll also need:

- **PHP 8.2+** (uses class-level `readonly` syntax)
- **A reachable OpenSalesTax engine.** Self-host with the [engine's docker-compose](https://github.com/ejosterberg/open-sales-tax) — about 5 minutes if you have Docker.

Production deployment guide
---------------------------

[](#production-deployment-guide)

For an existing PHP SaaS already using Stripe for billing. **~30 minutes** if your codebase is tidy.

### Step 1 — Spin up an OpenSalesTax engine

[](#step-1--spin-up-an-opensalestax-engine)

```
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",...}
```

Production: deploy the same compose file to your own VM, point a hostname at it, lock down with a firewall or reverse proxy.

### Step 2 — Collect billing addresses on checkout

[](#step-2--collect-billing-addresses-on-checkout)

If your Stripe Checkout sessions don't already require a billing address, add one line to `Session::create()`:

```
'billing_address_collection' => 'required',
```

Stripe persists the address on the Customer object; subsequent recurring invoices have it automatically — no further work needed.

### Step 3 — Add an invoice.created webhook handler

[](#step-3--add-an-invoicecreated-webhook-handler)

In your existing Stripe webhook router, add the case:

```
case 'invoice.created':
    self::applyOpenSalesTax($event->data->object);
    break;
```

### Step 4 — Implement applyOpenSalesTax

[](#step-4--implement-applyopensalestax)

```
use OpenSalesTax\Client as OpenSalesTaxClient;
use OpenSalesTax\Stripe\InvoiceCalculator;
use OpenSalesTax\Stripe\Exceptions\NonUSDException;
use OpenSalesTax\Stripe\Exceptions\MissingAddressException;

private static function applyOpenSalesTax(\Stripe\Invoice $invoice): void
{
    $client = new OpenSalesTaxClient(baseUrl: getenv('OPENSALESTAX_BASE_URL'));

    try {
        // Re-retrieve with expanded customer to ensure address is populated.
        $expanded = \Stripe\Invoice::retrieve([
            'id'     => $invoice->id,
            'expand' => ['customer'],
        ]);

        $breakdown = InvoiceCalculator::calculateForInvoice($expanded, $client);

        if ($breakdown->lines === [] || (float) $breakdown->taxTotal  'Sales Tax',
            'percentage'   => (float) $breakdown->combinedRatePct,
            'inclusive'    => false,
            'jurisdiction' => 'US',
        ]);

        \Stripe\Invoice::update($invoice->id, [
            'default_tax_rates' => [$taxRate->id],
        ]);
    } catch (NonUSDException | MissingAddressException $e) {
        // Best-effort: log and let the invoice proceed without tax.
        // (Merchant handles compliance manually for these edge cases.)
        error_log('OST skip: ' . $e->getMessage());
    } catch (\Throwable $e) {
        error_log('OST error: ' . $e->getMessage());
    }
}
```

### Step 5 — Set OPENSALESTAX\_BASE\_URL on production

[](#step-5--set-opensalestax_base_url-on-production)

```
OPENSALESTAX_BASE_URL=http://your-engine:8080
OPENSALESTAX_API_KEY=          # optional; only set if your engine has API-key auth enabled

```

That's it. The next subscription invoice fired will be auto-taxed.

Quickstart (just the calculator, no webhook integration)
--------------------------------------------------------

[](#quickstart-just-the-calculator-no-webhook-integration)

If you just want to compute tax on demand:

```
use OpenSalesTax\Client as OpenSalesTaxClient;
use OpenSalesTax\Stripe\InvoiceCalculator;

$ost = new OpenSalesTaxClient(baseUrl: 'http://localhost:8080');
$invoice = \Stripe\Invoice::retrieve($invoiceId);

$breakdown = InvoiceCalculator::calculateForInvoice($invoice, $ost);

echo $breakdown->subtotal;          // "100.00"
echo $breakdown->taxTotal;          // "8.025"
echo $breakdown->combinedRatePct;   // "8.025"

foreach ($breakdown->lines as $line) {
    echo "  {$line['stripe_line_id']} ({$line['category']}): \${$line['tax']}\n";
    if ($line['note'] !== null) {
        echo "    → {$line['note']}\n";
    }
}

foreach ($breakdown->jurisdictions as $j) {
    echo "  {$j['type']:9} {$j['name']:50} {$j['rate_pct']}% \${$j['tax']}\n";
}
```

For Checkout Sessions (instead of invoices):

```
use OpenSalesTax\Stripe\SessionCalculator;

$session = \Stripe\Checkout\Session::retrieve([
    'id'     => $sessionId,
    'expand' => ['line_items', 'line_items.data.price'],   // required: expand line_items
]);

$breakdown = SessionCalculator::calculateForCheckoutSession($session, $ost);
```

Stripe Tax Code mapping
-----------------------

[](#stripe-tax-code-mapping)

Each Stripe line item can carry a `tax_code` on its `Price` (e.g. `txcd_10103001` for SaaS Business). The connector translates Stripe's catalog into OpenSalesTax's 6 categories. Default mapping:

Stripe codeOST category`txcd_99999999` General Tangible Goods`general``txcd_20030000` General Services`general``txcd_10103001` SaaS Business / `txcd_10103000` SaaS Personal`digital_goods``txcd_10101000` IaaS / `txcd_10102000` PaaS / `txcd_10105002` AIaaS`digital_goods``txcd_10302000` Digital Books / `txcd_10402100` Digital Video / `txcd_10401100` Digital Audio / `txcd_10701100` Website Hosting`digital_goods``txcd_00000000` Nontaxableline is **excluded** from the OST calc; tax = 0anything elsefalls through to `general` (a warning callback can capture these)Override the mapping for your own catalog:

```
use OpenSalesTax\Stripe\TaxCodeMap;

$customMap = new TaxCodeMap([
    'txcd_30060006' => 'clothing',
    'txcd_40030003' => 'prepared_food',
]);

$breakdown = InvoiceCalculator::calculateForInvoice($invoice, $ost, $customMap);
```

The default table covers SaaS / digital products. For physical-goods merchants, expect to add specific Stripe codes for clothing / groceries / etc. depending on your catalog.

How this compares
-----------------

[](#how-this-compares)

CostSelf-hostedPer-jurisdiction breakdownStripe-native flow**Stripe Tax**0.5%/txn❌✅✅**TaxJar API**from ~$0.50/txn❌✅partial**Avalara AvaTax**enterprise pricing❌✅partial**OpenSalesTax + this connector**$0 software✅✅via `TaxRate.create`OpenSalesTax + this connector trades operational responsibility (run a small engine VM) for transactional cost (zero). For SaaS founders comfortable with Docker, that's an easy win above ~$50K MRR.

What's NOT in v0.1
------------------

[](#whats-not-in-v01)

- **Webhook handler / signature verification** — you write the webhook plumbing; this library is the calculator. v0.2 will add a self-contained webhook handler.
- **Stripe Tax-compatible response shim** — exposing the same response shape Stripe's `/v1/tax/calculations` returns, so SaaS already integrating Stripe Tax can switch with one config line. v0.3 marketing pitch.
- **Non-USD invoices** — engine is USD-only; non-USD invoices throw `NonUSDException` (callers can log + skip).
- **Stripe Connect / multi-account** — out of scope; multi-account flows have meaningfully different tax-collection-responsibility semantics.
- **Subscription proration mid-cycle** — calculates at invoice-creation point; for partial-period invoices the tax is point-in-time correct but proration semantics aren't formally validated.

Quality bar
-----------

[](#quality-bar)

- **PHPStan level=max** — zero suppressed errors
- **PHP-CS-Fixer** with PSR-12 + risky rules — zero violations
- **PHPUnit** — 29 unit tests, 100 assertions, all passing
- **GitHub Actions CI** matrix on PHP 8.2 / 8.3 / 8.4
- **DCO sign-off** required on every commit

Engine compatibility
--------------------

[](#engine-compatibility)

Tested against **OpenSalesTax v0.24**. Engine v0.22+ recommended for production (a state-bleed data bug was fixed in v0.22). Pin both:

```
ejosterberg/opensalestax-stripe-php: ^0.1
opensalestax engine:                 v0.22+

```

Disclaimer
----------

[](#disclaimer)

> Tax calculations are provided as-is for convenience. The merchant is solely responsible for tax-collection accuracy and remittance to the appropriate jurisdictions. Verify against your state Department of Revenue before remitting.

Contributing
------------

[](#contributing)

DCO sign-off (`git commit -s`) required on every commit. See [CONTRIBUTING.md](CONTRIBUTING.md). Dual-licensed Apache-2.0 OR GPL-2.0-or-later + SPDX header on every source file.

License
-------

[](#license)

Dual-licensed under your choice of [Apache-2.0](LICENSE-APACHE.txt) OR [GPL-2.0-or-later](LICENSE-GPL.txt). See [LICENSE](LICENSE).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance95

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

5

Last Release

21d ago

PHP version history (2 changes)v0.1.0PHP &gt;=8.1

v0.1.1PHP &gt;=8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/b647ae9c341fdf92c36cb479ba7535c2eff9ac92f8614965a3b5953f7b543152?d=identicon)[ejosterberg](/maintainers/ejosterberg)

---

Top Contributors

[![ejosterberg](https://avatars.githubusercontent.com/u/6363457?v=4)](https://github.com/ejosterberg "ejosterberg (10 commits)")

---

Tags

stripesaastaxsales taxopensalestaxstripe-tax

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ejosterberg-opensalestax-stripe-php/health.svg)

```
[![Health](https://phpackages.com/badges/ejosterberg-opensalestax-stripe-php/health.svg)](https://phpackages.com/packages/ejosterberg-opensalestax-stripe-php)
```

###  Alternatives

[duncanmcclean/statamic-cargo

Comprehensive e-commerce addon for Statamic. Build bespoke e-commerce sites without the complexity.

3310.1k](/packages/duncanmcclean-statamic-cargo)[flux-se/sylius-stripe-plugin

Sylius Stripe plugin using Payment Request

1136.3k](/packages/flux-se-sylius-stripe-plugin)[tastyigniter/ti-ext-payregister

Allows you to accept credit card payments using PayPal, Stripe, Authorize.Net and/or Mollie.

1016.7k6](/packages/tastyigniter-ti-ext-payregister)[helori/laravel-saas

Software as a Service scaffholding for Laravel based on Vue 3, Tailwindcss and Stripe. Inspired by Laravel Jetstream and Spark.

121.3k](/packages/helori-laravel-saas)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
