Skip to content

Seller registrations

Seller registrations

Every assessment carries the selling entity, as SellerRegistrations. In a multi-tenant application each tenant is a different entity with different registrations, so this travels with the query, never in configuration.

use Cbox\Geo\ValueObjects\CountryCode;
use Cbox\Geo\ValueObjects\SubdivisionCode;
use Cbox\Tax\Regime\UsSalesTaxRegime;
use Cbox\Tax\ValueObjects\OssStatus;
use Cbox\Tax\ValueObjects\SellerRegistration;
use Cbox\Tax\ValueObjects\SellerRegistrations;

$seller = new SellerRegistrations(
    establishment: new CountryCode('DK'),
    registrations: [
        // A foreign VAT/GST number: the seller collects there.
        new SellerRegistration(new CountryCode('NO')),

        // A US state permit. This IS nexus — see below.
        new SellerRegistration(new CountryCode('US'), new SubdivisionCode('US-TX')),

        // A Canadian province's PST/QST permit, which GST registration is not.
        new SellerRegistration(new CountryCode('CA'), new SubdivisionCode('CA-BC')),

        // A scheme registration: one number, the whole Union.
        new SellerRegistration(new CountryCode('IE'), scheme: 'ioss'),

        // A state's flat-rate election for remote sellers, from the day it took effect.
        new SellerRegistration(
            new CountryCode('US'),
            new SubdivisionCode('US-TX'),
            scheme: UsSalesTaxRegime::REMOTE_ELECTION_SCHEME,
            validFrom: new DateTimeImmutable('2026-01-01'),
        ),
    ],
    oss: new OssStatus(registered: true),
);

Nothing here is inferred. A seller that states nothing is a seller established in one country and registered nowhere else, and that is priced as exactly that.

Establishment decides the regime's questions; registration decides collection

establishment is where the selling entity belongs. It settles whether a supply is cross-border — which is what a reverse charge turns on — and, inside the EU, whether the seller is on the Union's own rules. Establishment implies registration at home: a Danish company does not state a Danish VAT number to charge Danish VAT.

registrations are everywhere else it has a number. Outside the United States, a supply that would otherwise be taxed at destination is returned as NotRegistered when the seller is neither established nor registered there:

// A Danish shop selling an e-book to a Norwegian consumer, with no Norwegian registration.
$assessment->treatment;            // TaxTreatment::NotRegistered
$assessment->tax->getAmount();     // 0.00

That is not "no tax was due". Norwegian VAT is due — Norway asks a foreign seller of e-books to register from the first sale, and this seller has no number to remit it under. Add a SellerRegistration for Norway and the same query charges 25%. The distinction matters on a return, which is why NotRegistered is its own treatment rather than a zero: see marketplace facilitator for the four zeros that mean different things.

Two ways in are treated as registration across the whole Union, because that is what they are: an OssStatus with registered: true, or a registration whose scheme is oss, wherever it is filed. Both are read on the supply's date.

An EU-established seller's goods shipped outside the Union are not NotRegistered at all: they come back ZeroRated in the seller's own member state as an exempt export (Art. 146), with an export invoice mention. See regimes.

IOSS and the €150 consignment

An ioss registration is narrower than OSS. It collects the customer's VAT at checkout only on goods imported into the Union in a consignment of an intrinsic value of at most €150, and never on goods subject to excise duty — alcohol, tobacco, energy products (Art. 369l). Outside that, the seller charges nothing and the customer pays import VAT at the border; the reason names which condition applied.

  • The consignment is the document. A TaxOrder measures its goods lines together, delivery left out (TaxOrder::consignmentValue()): two €100 items are a €200 consignment. A single TaxQuery is measured on its own amount, or on consignmentValue where you pass it.
  • In euro, without VAT. A tax-inclusive price is measured net, and another currency is converted at the ExchangeRates rate for the tax date (the ECB's, once tax:fx:sync has run). With no rate the charge stands, flagged consignment_value_unmeasured.
  • Imported. Goods shipped from inside the Union (route->shipFrom in a member state) are not an import, and IOSS does not reach them; nor does it reach services.
// A UK shop with IOSS, a €400 order to a German consumer.
$assessment->treatment;   // TaxTreatment::NotRegistered — import VAT at the border
$assessment->reason;      // "IOSS covers a consignment only up to … EUR 150 (Art. 369l …)"

Sub-federal permits: US nexus and Canadian PST

A registration with a subdivision is a permit in that state or province, and it is the only thing that establishes an obligation there.

  • United States. The seller must hold the state's permit, or the supply comes back NotRegistered with the state's economic-nexus threshold quoted in the reason — see US economic-nexus thresholds. The permit also gates the state's delivery-charge rules: without it there is no charge to exclude freight from.
  • Canada. GST and HST are one federal registration, so a seller registered in Canada charges Ontario's 13%. A PST or QST is the province's own, and a seller that does not hold it charges the federal 5% in British Columbia, Saskatchewan, Manitoba and Quebec rather than the combined rate. State the province permit to collect it.

A registration for the country alone (new SellerRegistration(new CountryCode('US'))) is not a permit in any state and grants nexus nowhere.

Schemes

scheme records how the seller is registered, not merely that it is. Three values are read by the engine, and the rest are yours to keep for your own bookkeeping:

Scheme Read by What it does
oss the collection gate Collects across all 27 Member States from one number — for a seller established outside the Union, on services and on goods shipping from inside the Union, not on imported goods
ioss the collection gate Collects across all 27 Member States on imported goods in a consignment of at most €150, excise goods excepted
remote-election UsSalesTaxRegime Prices a state's flat-rate remote-seller programme instead of resolving the rate — Texas's single local rate, Alabama's SSUT

A remote-election scheme needs the subdivision as well; an election is a state's programme, not a country's. It is also skipped for a supply shipped from inside the destination state: those programmes exist for remote sellers, and in-state presence is priced the ordinary way.

Registrations have a lifetime

validFrom and validUntil are inclusive of their day, and null means open-ended. They are checked against the supply's tax point, not today.

This is what makes a backfill correct. A seller registered in Texas on 1 March did not owe Texas tax in February; without the window, recalculating last year's invoices applies today's registrations to them and produces a return for a period the seller was not registered in. The same applies in reverse to a number that lapsed. See dates for everything else resolved on the tax point.

new SellerRegistration(
    new CountryCode('US'),
    new SubdivisionCode('US-TX'),
    validFrom: new DateTimeImmutable('2026-03-01'),
    validUntil: new DateTimeImmutable('2026-12-31'),
);

The signals, and what reads each one

Signal Read by Decides
establishment every regime Cross-border, reverse charge, EU-established treatment
registrations[].country the collection gate Whether the seller collects in that country at all
registrations[].subdivision US and Canadian regimes Nexus in a state; a province's PST
registrations[].scheme the collection gate, UsSalesTaxRegime One-stop coverage; a flat-rate election
validFrom / validUntil all of the above Whether it was in force on the supply date
oss->registered the collection gate One-stop coverage of the Union
oss->thresholdExceeded EuVatRegime Destination sourcing under Art. 59c

The two OssStatus flags are not interchangeable. thresholdExceeded says the micro-business relief no longer applies, which moves the place of supply to the customer's Member State; it does not say the seller has a number to collect with. A seller over the threshold, not OSS-registered and not established in the Union is therefore sourced at destination and then returns NotRegistered — which is the honest answer, and the remedy is to register.

Asking what the seller holds

SellerRegistrations answers the same questions the engine asks, so an application can gate its own checkout on them:

$seller->isEstablishedIn(new CountryCode('DK'));                          // bool
$seller->isRegisteredIn(new CountryCode('NO'), $supplyDate);              // bool
$seller->isRegisteredInSubdivision(new SubdivisionCode('CA-BC'), $date);  // bool
$seller->holdsSubdivisionScheme(new SubdivisionCode('US-TX'), 'remote-election', $date);
$seller->holdsUnionScheme($date);                                         // OSS or IOSS
$seller->hasScheme('ioss', $date);                                        // any one scheme