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 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 — it is collected at the border from the buyer, because 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 or ioss, wherever it is filed. Both are read on the supply's date.

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, ioss the collection gate Collects across all 27 Member States from one number
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