Skip to main content

Upgrading

What each released version asks of you, newest first. Most ask for nothing beyond running the migrations.

Before 1.0, a minor may break

The package is pre-1.0, so a minor version is allowed to change a published API. Every such change is listed in the CHANGELOG under a heading that says so, and repeated here with what to do about it. Pin a minor if you want no surprises:

"pushery/billing-for-laravel": "~0.13.0"

Every upgrade ends the same way, because a minor may add tables or columns:

composer update pushery/billing-for-laravel
php artisan migrate

0.27.0

Run the migrations

billing_merchant_charges gains transfer_failed_at, transfer_failure and seller_posture. The first two record a merchant share the provider refused to move; the third records who supplied the buyer, which is what the small-business counter now reads.

A reporting renderer of your own is handed only the reportable sellers

Affects you only if you bind your own RendersReportingRecord. It used to receive every seller the period examined, with a paragraph asking you to filter on reportable() before transmitting. It now receives the reportable sellers and their records and nothing else, and seller_count counts those.

If your renderer keeps the record of the due diligence — which has to show every seller it was applied to — implement RendersDueDiligenceRecord instead, and only for an artifact nobody transmits.

Creators cross the small-business limit earlier under seller_of_record and platform_intermediary

The earnings counter and SmallBusinessThresholdMonitor counted every routed sale at its payout, which is right only under the commission chain. Where the creator supplies the buyer, the platform's commission is a service they bought and stays in their own turnover, so those creators were under-counted by the whole commission. Nothing to configure; expect the threshold warnings to arrive sooner, which is the correction.

A failed merchant transfer no longer throws at your caller

RoutedPayment::charge() used to throw when the share could not be moved after the buyer had paid, leaving the sale pending for good. It now returns the successful payment, reports the failure to your exception handler, writes it to the row and dispatches MerchantShareNotMoved. Listen for that event, watch billing:doctor, and move the shares with billing:marketplace:retry-transfers.

A margin-taxed document you issue yourself is stricter, in both halves

MarginRefundCorrector refuses a document that was not taxed on the margin, and one with no frozen margin_minor, where it used to correct on a margin of zero. A margin-taxed InvoiceRecord that states a tax amount is refused when it is created rather than when it is rendered. In XRechnung and ZUGFeRD such a document is category E with VATEX-EU-F and the prescribed wording, where it used to go out as zero-rated Z; one that also names an exemption, or whose lines carry a tax rate, is refused. See Taxes.

A money-credit top-up is not taxed at the till any more

An add-on that grants no usage units credits the owner's balance, and that balance is a voucher: the invoice it later pays carries the tax. Under a provider tax mode the hosted checkout no longer asks the provider to tax the top-up itself. Where you sell into one country at one rate, set BILLING_VOUCHER_INSTRUMENT_TYPE=single_purpose and it is taxed at issue again.

Import the announced monthly averages, or foreign-currency documents will refuse

Affects you only if you issue documents under a monthly-average conversion rule — the German profile does, for domestic turnover.

The reader used to take the arithmetic mean of the central bank's published days for the month and answer the monthly rule with it. That is the aggregation an authority's table is made of, and it is not that table: the two part company as soon as a day is missing locally, an observation is revised, or the rounding differs by a digit, and the difference lands on a document that is checked against the official figure years later.

A month now needs an announced average in the store, and there is a command to put one there:

php artisan billing:exchange-rates:import-file storage/rates/2026-monthly.csv --source=BMF

The file carries from,to,month,rate — see the command reference. Until a month is imported, a conversion under that rule is refused rather than computed, which is the same refusal a missing daily rate has always produced.

0.26.0

Run the migrations

billing_tax_id_verifications is new. It keeps what a tax authority's register said about a buyer's tax ID, one row per answer, and an erasure unlinks it rather than deleting it, like the invoices it supports.

Send the tax ID events to your Stripe endpoint

Stripe reports the register's answer with customer.tax_id.created and customer.tax_id.updated. An endpoint that receives only the events it was set up with does not see them until both are added. Listen for TaxIdVerificationFailed to learn about a number the register does not know, together with the invoices issued with the charge reversed under it.

0.25.0

An implementation of Checkout or OneTimeCharge declares $buyerCountry

Affects you only if you implement one of the two contracts. Code that only calls them, and the shipped drivers, need nothing.

Checkout::subscribe() and OneTimeCharge::purchase() take the buyer's country as a new last parameter, and PHP refuses an implementation that does not accept it:

public function subscribe(Model $billable, string $tierKey, ?string $couponCode = null, ?string $declarationReference = null, ?string $buyerCountry = null): ClientIntent;

public function purchase(Model $billable, string $addonKey, ?string $declarationReference = null, ?string $buyerCountry = null): ClientIntent;

An implementation of MerchantPriceProvisioner accepts a null interval

Affects you only if you implement the contract. A null interval now asks for a one-time price, so PHP refuses an implementation that still declares a non-nullable interval:

public function provision(Model $merchant, string $tierKey, Money $amount, ?BillingInterval $interval): string;

A sale into a market that is not open is undone

Affects you only if you configure billing.tax_markets and sell through Stripe. When Stripe reports that it taxed a sale in a country the map does not open, the package ends the subscription, refunds the payment and dispatches SaleIntoClosedMarketReversed. Listen for it to tell the buyer. Without a market map nothing changes.

An implementation of DiscountResolver accepts the sale's scope

Affects you only if you bound a resolver of your own. Code that only calls resolve(), and the shipped resolvers, need nothing.

DiscountResolver::resolve() takes the seller of the sale as a new last parameter, and PHP refuses an implementation that does not accept it:

public function resolve(string $code, ?MerchantScope $merchant = null): ?Discount;

A null scope is the platform. A resolver with no notion of an issuer may ignore it, the way ConfigDiscountResolver does.

A coupon row resolves without a config entry

Affects you if you keep coupons in billing_coupons. A live row now resolves its code for the seller who issued it, before billing.coupons is asked, and the Stripe checkout applies its provider_coupon_id with no config entry at all. An installation that keeps its coupons only in config sees no change.

0.24.0

A driver of your own answers ReadsSubscriptionPayments

Affects you only if you ship a billing driver of your own. The Stripe driver and the local-engine driver bind it themselves.

ConsumerWithdrawal::withdrawSubscription() reads the payment behind a subscription's period in progress through Contracts\ReadsSubscriptionPayments. The Stripe provider binds its reader unconditionally, so a driver of your own that leaves the contract alone gets that reader, and it asks Stripe about subscriptions Stripe never saw. Bind yours next to your SubscriptionActions. A driver that collects a period at its end can bind LocalSubscriptionPayments, which answers that no payment covers the period in progress.

The migrations add started_at to billing_subscriptions, nullable.

0.23.0

A custom Checkout or StartsSubscriptions takes the declarations key

Affects you only if you implement Contracts\Checkout or Contracts\StartsSubscriptions yourself. Code that calls them, and the shipped drivers, need nothing.

subscribe() and start() take a fourth, optional parameter: the key a buyer's withdrawal declarations were recorded under. PHP refuses an implementation that does not accept it, so add it to yours:

public function subscribe(Model $billable, string $tierKey, ?string $couponCode = null, ?string $declarationReference = null): ClientIntent

public function start(Model $billable, string $tierKey, ?string $couponCode = null, ?string $declarationReference = null): SubscriptionStart

Then hand it to wherever your implementation creates the subscription, so the local row ends up carrying it. An implementation that accepts the key and drops it still runs; WithdrawalConsentLedger::forSubscription() simply answers null for every subscription it starts.

The migrations add declaration_reference to billing_subscriptions and billing_subscription_intents, both nullable.

0.14.0

The sale's tax characteristics travel as one object

Affects you if you call FanReceiptIssuer::issue(), SelfBillingEngine::issue() or any of SubscriptionCycleBilling's three issuing methods directly. Through the hosted lanes there is nothing to do.

Those five signatures each took the same run of tax primitives — ?TaxArchetype $archetype, ?PlaceOfSupplyRule $placeOfSupply, ?TaxRateCategory $rateCategory and, on the receipt issuer, five more. They now take one SupplyTaxCharacteristics:

// before
$receipts->issue($buyer, $tier, $gross, $bps, $soldOn, $ref, null, $period, $archetype, $place, $band);

// after
$receipts->issue($buyer, $tier, $gross, $bps, $soldOn, $ref, period: $period, characteristics: new SupplyTaxCharacteristics(
archetype: $archetype,
placeOfSupply: $place,
rateCategory: $band,
));

Every field is nullable and defaults to null, so a caller that passed none of them needs no change at all: SupplyTaxCharacteristics::unknown() and simply omitting the argument write the same row.

Why this is worth the edit. The receipt issuer's signature was seventeen parameters wide and two callers filled it positionally. A parameter inserted in its middle shifted every argument after it past two type-compatible pairs — ?CarbonImmutable $deliveredOn against CarbonImmutable $soldOn, and ?string $chargeReference against ?string $provider. Static analysis cannot tell those apart, and a document that comes out with the wrong date is a tax error rather than a display one.

It also widens what a subscription cycle can state: it could reach three of the eight characteristics and now reaches all eight, so a cycle that knows its delivery date or its exemption reason can say so.

Support\Navigation and NavItem are gone

Affects you if you resolved either class. Use Pushery\Billing\Account\Navigationvisible() for the grouped form the sidebar renders, visibleItems() for a flat list.

They were a second parser of config('billing.navigation'), and the two disagreed: the surviving one knows the web_only flag and drops such an item on a native runtime, the removed one had no property to carry it. An operator who hid the account-deletion flow saw it leave the sidebar and stay on the hub's landing page, one click from a working deletion. Removing the second parser is what stops that recurring; teaching it the flag would have left two readings of one key in place.

Some value objects and events carry more required arguments

Affects you only if you construct any of these by hand. Resolved from the container, or read from an event you receive, nothing changes — the package fills the new arguments itself.

Each gained required constructor arguments because it now carries a fact it previously had nowhere to put:

ClassRequired arguments
ValueObjects\InboundTaxTreatment5 → 11
ValueObjects\WithdrawalConsent4 → 8
ValueObjects\RetentionRule13 → 15
Events\AddonPurchased4 → 7
Notifications\PaymentSucceededNotification2 → 9
Marketplace\BuyerProtectionClock1 → 7

Service constructors widened as well — StripeDriver, StripeOneTimeCharge, StripePaymentMethods, BillingAdmin, EuOssTaxCalculator, TaxCalculatorFactory — but every documented usage resolves those from the container, so they are named here for completeness rather than as something to do.

Three methods were removed because nothing read them: SellerActivityThreshold::isExemptFromReporting(), InvoiceNumberSequence::format() and RateChangeExclusions::dryRunRequired(). The purging() helpers on BillingEvent and TaxReturnExportRecord did not disappear — they moved into the shared AppendOnly concern and are still callable on both models.

ConsumerWithdrawal takes two collaborators fewer

Affects you only if you construct it by hand — resolved from the container, nothing changes.

It no longer takes a RoutedRefundCorrector or a CreatorTaxStatusResolver. The chain correction moved into BillingAdmin::refund(), which is where it belonged: that verb is the package's only refund entry point and was the one path of four that corrected no documents. Leaving the correction in both places would have written two correcting documents per leg for one event, out of a gapless number series.

0.13.0

This release carries the work stamped 0.10.0, 0.11.0 and 0.12.0. None of the three was ever tagged, so v0.9.0 is the last published version before this one and upgrading from it lands you here in one step. The changelog folds them the same way and for the same reason: there is no release a reader could be sitting on in between, so three sets of instructions were three descriptions of one upgrade.

Six items, and all but one need a decision before you deploy — the toast change matters only if you listen for the browser event yourself. Anything not listed here is additive. Run the migrations last.

1. A routed one-time sale now issues the buyer's receipt, and needs to be told who the buyer is

Affects you if you call RoutedPayment::charge(), FanPayment::tip() or FanPayment::payWhatYouWant() directly. If you only use the hosted checkout, there is nothing to do.

Until now this lane charged the buyer, settled the sale and paid the merchant their share without producing any document. A fan who bought once had no receipt, and the sale's supply regime was written down nowhere — the charge table has no column for it, so the document is the only place it can be frozen.

Issuing it needs two facts the package cannot invent: who the buyer is, and whether the small-value rules of your own country apply to them (which decides the document tier). Both are now required parameters:

// before
$payments->charge($merchant, $gross, $fee, $taxBps, $token, $routing, $archetype);

// after
$payments->charge($merchant, $buyer, $gross, $fee, $taxBps, $buyerIsDomestic, $token, $routing, $archetype);

On FanPayment the buyer sits after the merchant, and the flag after the existing TaxContext:

$fan->tip($merchant, $buyer, $chosen, $normalFee, $taxContext, $buyerIsDomestic, $token, $routing, $soldAlongside);
$fan->payWhatYouWant($merchant, $buyer, $chosen, $fee, $taxContext, $buyerIsDomestic, $token, $routing, $archetype);

$buyer is the model the receipt belongs to — the same one you would pass to the subscription lane. $buyerIsDomestic is supplied rather than derived, exactly as SubscriptionCycleBilling already takes it: the package has no second opinion about where your buyer is, and inventing one would give the two receipt-issuing lanes two different answers.

Why both are mandatory rather than optional with a default. An optional buyer would mean the receipt goes on being skipped for every call site not yet updated — silently, and for precisely the sales that already work today. Required, your existing positional call raises a TypeError on the first run instead, which is the one failure mode you cannot miss.

The document is issued only after the provider confirms the payment succeeded, so a declined or still pending charge produces nothing. It is idempotent on the charge reference: a redelivered webhook returns the document already written rather than drawing a second number from a series that must have no gaps.

One posture issues nothing, by design. Where you have declared the merchant as seller of record, the platform is not a party to the supply and has no document to issue. Where the platform merely arranges, the intermediation receipt is not wired yet — it states the commission's own tax rate, which is still an open question at that seam.

2. The realtime toast names its severity differently

Only affects you if you listen for the wirekit-toast browser event yourself. If a WireKit host or this package's own opt-in region renders your toasts, there is nothing to do.

The bridge used to dispatch the severity as detail.level. A toast region reads detail.variant, so the severity never arrived: every toast rendered in the neutral style, and a failed payment was announced to a screen reader without urgency. It now dispatches detail.variant. Change your handler to read that.

The broadcast payload is unchanged — AccountToastNotified still sends { message, level }. Only the browser event moved, because only that end has a reader that decides what the key means.

3. A routed subscription on the shipped defaults now refuses

Only affects you if billing.marketplace.enabled is true. With it false — the default — nothing changes.

StripeCheckout used to check the configured charge type and then assemble a payload that ignored it. The shipped charge_type is separate_transfer, which the posture table permits for platform_deemed_supplier, so the guard passed — while the session it opened carried transfer_data.destination, which is a destination charge, and destination + platform_deemed_supplier is precisely the pairing billing.marketplace.charge_type_by_posture forbids. The money went straight to the merchant while the documents named the platform as seller.

That combination now throws MarketplaceUnsupported instead of opening a session. Two ways forward:

  • Move the merchant's share yourself. Keep charge_type on separate_transfer and route the sale through Pushery\Billing\Marketplace\RoutedPayment, which makes both provider calls and records them.
  • Use a destination charge honestly. Set charge_type to destination and a posture the table permits for it — platform_intermediary, or seller_of_record if the Art. 9a rebuttal genuinely holds for you (the package refuses seller_of_record for an electronically-supplied service unless you assert it).

If you were running the old default and your Stripe payouts reconciled, they were reconciling against a shape the package's own configuration disallowed. That is the change worth reviewing before you deploy.

Later lifted. The hosted subscription checkout now serves separate_transfer itself: the session carries no routing, the merchant's account and the frozen fee terms ride in the subscription's metadata, and every paid cycle writes its ledger row and moves the merchant's share. On the shipped defaults it opens a session again, this time on the lane the table permits for the posture.

4. ExchangeRateBasis::MinistryMonthlyAverage is renamed

It is now CentralBankMonthlyAverage, named after the source rather than the place of publication. The old name described a table nobody could actually fetch, which is what made an unsupplied basis look supplied.

No data migration is needed: nothing ever wrote the old ministry_monthly_average value, so no stored row carries it. Update any code that names the case; a match over the enum will fail to compile rather than fall through, which is the intended way to find them.

The monthly average is now computed from the daily series the package already imports — the arithmetic mean of the month's published rates, averaged over what was published rather than divided by the calendar.

5. billing.tax_oss.required_signals is removed

It was read in exactly one place, and only to describe a decision it had no part in — and the expression could not represent 3 at all, so a valid standard of three sources was written as 2. Both halves now read the same standard through one shared reader.

Delete the key from your published config. Leaving it does nothing; the package no longer reads it.

6. Run the migrations

This release adds columns rather than changing existing ones — a subdivision on the place evidence, a rounding direction on the merchant charge, a seller posture and a tax-exemption reason on issued documents:

composer update pushery/billing-for-laravel
php artisan migrate

billing_place_evidence.resolved_subdivision is written only for a country listed in billing.tax_evidence.subdivision_countries (shipped: the US alone) and only from a subdivision you already supply — the package has no input finer than the country and does not go looking for one. Every non-US sale is byte-identical to before, and billing.tax_evidence.collect_subdivision switches the whole thing off.

0.9.0

Nothing to do. Documentation only: the pages that described unshipped features are gone, and the configuration, database, event and troubleshooting references are written from the code. No code, config or schema changed.

One key that the boot guard already read is now declared in the published config — billing.retention.allow_below_statutory_minimum, default false. Behavior is unchanged; it was previously discoverable only from the exception message. Re-publish the config to pick up the declaration, or ignore it: the package merges its own defaults underneath yours.

0.8.0

If you mapped Money to a decimal-string amount shape, that pair of methods is gone. It had no shipped consumer, so it was removed before anything could depend on it. Use Money::toDecimal() and Money::fromDecimal(), which are unchanged.

Everything else: nothing to do. Amounts remain integer minor units end to end.

0.7.0

If you ship your own driver implementing SubscriptionActions, add the new optional parameter:

public function cancel(Model $billable, ?CancellationSurvey $survey = null): void

Callers are unaffected — the argument defaults to null — and the built-in drivers already have it. A driver that ignores the survey is a valid driver; the parameter only has to exist so the contract is satisfied.

Run the migrations: 0.7.0 adds the cancellation-survey table.

0.6.0

The largest upgrade so far. Three things need attention.

The cancellation "credit note" is now an "invoice correction". Credit note is reserved for the self-billing document, which is a different document with a different type code. Rename your references:

RemovedUse instead
ValueObjects\CreditNoteSnapshotValueObjects\InvoiceCorrectionSnapshot
Events\InvoiceCreditedEvents\InvoiceCorrected, reading $event->correction (was $event->creditNote)
Webhooks\Effects\PersistCreditNoteWebhooks\Effects\PersistInvoiceCorrection
InvoiceRecord::isCreditNote()InvoiceRecord::isCorrection()
translation key billing::invoice.credit_notebilling::invoice.correction

The event is the gentle one: InvoiceCorrected also fires InvoiceCredited for one deprecation window, so an existing listener keeps being called rather than going quiet. The value object, effect and model method are hard renames — a stale reference is a loud "class not found", not a silent no-op. See the event reference.

The invoice retention floor dropped from ten years to eight, and the clock now runs from the end of the issue year rather than the issue instant. An erased owner's retained invoices become prunable up to two years earlier than before. That is the point: keeping them the full ten years over-retains personal data past its obligation. If your jurisdiction requires longer, set billing.retention.erased_financial_days higher — a longer window is always allowed. The separate audit window stays at ten years.

InvoiceCorrectionSnapshot now validates itself. It refuses a negative amount (a correction carries positive magnitudes; the document's nature inverts the meaning, not the sign) and refuses an amendment with no reference to the invoice it corrects. If you construct snapshots yourself, pass absolute amounts.

Also worth knowing, though neither needs action: the documentation moved out of the README into docs/, and several tables were added — run the migrations.

0.5.0

If you keep golden copies of DATEV exports, regenerate them. The EXTF header's Festschreibekennzeichen was emitted as 0, marking a booking batch as still alterable after import. It is now 1, which changes the bytes of every generated file.

Your app may now refuse to boot where it previously started. Two silent failures became loud:

  • an unresolvable billing.tax — a typo, or the key turned into an array by adding a sub-key under it — now raises TaxModeUnsupported at boot instead of falling through to "no tax" and issuing every invoice at 0%
  • billing.tax = 'stripe' is now correctly classified as provider tax, so it is accepted on the driver that needs it and refused on one that cannot apply it

A malformed country code now throws instead of zero-rating. EuOssTaxCalculator treated any code it had no rate for as zero-rated, so "DEU" or an empty string was indistinguishable from a genuine supply outside the EU VAT area. An unassigned code now raises UnknownTaxCountry. Real countries outside the EU VAT area are still zero-rated. If your data carries three-letter or full-name country codes, normalize them to ISO 3166-1 alpha-2 before this upgrade.

0.4.0 and 0.4.1

Nothing to do. 0.4.1 is release-note housekeeping with no functional change.

0.4.0 adds the opt-in billing.marketplace config block, off by default, so single-merchant behavior is unchanged. It also adds a billing umbrella publish tag — php artisan vendor:publish --tag=billing now publishes config, migrations, views and translations in one go, and the specific tags still work.

Contributors only: the static-analysis composer script was renamed to analyze.

0.3.0

Nothing to do. The admin console and the ZUGFeRD PDF/A-3 writer are both additive and both optional.

The hybrid PDF/A-3 needs a real PDF toolchain, so it is a suggested dependency: run composer require horstoeko/zugferd to use it. Without it the method throws MissingPdfEmbedder rather than fataling on an undefined class. The XML writers need none of it.

0.2.0

Check your VAT setup — this release closed two under-charging holes, and both change what customers are billed.

  • The EU reverse charge now requires a validated VAT id. Previously any supplied id, verified or not, earned the zero-rate. The default VatIdValidator proves nothing, so the zero-rate is not granted until you bind a real validator. Bind ViesVatIdValidator if you sell B2B across EU borders; otherwise your business customers will now be charged domestic VAT.
  • A domestic B2B sale is no longer zero-rated. The reverse charge applies only when the buyer's country differs from billing.company.country. Set that key — when the seller country is unknown, nothing is zero-rated, which is the safe direction but probably not what you want.

Also in this release: reverse-charge invoices no longer leak VAT into their totals (an EN 16931 violation a validator rejects), the EU-OSS table is matched case-insensitively, and a VIES outage is treated as unavailable rather than invalid. Every provider link-out is now scheme-validated before the redirect.

Run the migrations: 0.2.0 adds the coupon tables and the e-invoicing columns.

0.1.1

If your app deletes accounts from its own flow, dispatch BillableAccountDeleting before you delete the model:

use Pushery\Billing\Events\BillableAccountDeleting;

event(new BillableAccountDeleting($user));
$user->delete();

Without it, a deleted owner stays active and charging at the provider. The package's own eraser dispatches it for you; a custom delete button does not. Dispatch it after re-confirming identity and before the delete, so the listener can still resolve the owner's provider reference.

If you ship your own driver

A driver is a set of contract implementations, so what an upgrade asks of you is exactly which contracts moved. Across the versions above:

  • 0.7.0SubscriptionActions::cancel() gained ?CancellationSurvey $survey = null
  • 0.6.0PersistCreditNote became PersistInvoiceCorrection, and InvoiceCredited became InvoiceCorrected; a driver's webhook mapper that produced the old event must produce the new one
  • 0.5.0 — nothing on the driver contracts, but a driver that reports no provider tax will now be refused at boot if billing.tax is provider, and one that defers tax will be refused on a local mode

Two contracts are worth re-reading after any upgrade because their guarantees are what the fail-closed guards check: PaymentRails (moves money, stores mandates) and BillingEngine (the recurring cycle). PaymentRails is deliberately not eligibility-gated — the gate belongs at the entry seams where a payment begins, so that a dunning retry for a subscriber who was eligible when they subscribed is never refused later.

See the contract reference for what each seam guarantees.

The marketplace surface is opt-in

The multi-merchant marketplace (Stripe Connect) is additive and does nothing until you turn it on. Three things a driver author needs to know:

  • Nothing existing was widened to make room for it. PaymentRails, BillingDriver and the argument order of ChargeResult are untouched. The routing dimension arrives as an optional trailing ?ChargeRouting $routing = null on the money methods, defaulting to null — which is exactly today's behavior. A payment with no routing reaches the provider with exactly the fields it always has.
  • The routing capability is opt-in, at the driver. A driver joins the marketplace by implementing the RoutesMoney contract; one that does not implement it cannot produce a rails object, so no configuration can route through it. If you have written your own driver, it keeps working unchanged as a single-seller driver until you choose to implement RoutesMoney.
  • A driver that cannot serve a routing must THROW, never no-op. This is the one that loses money if you get it wrong. A routing the driver silently ignores settles the whole payment on the platform account, and the merchant is never paid — with nothing in the result saying so. The failure has to be loud: refuse the operation rather than complete it as an unrouted charge.

And two things an APPLICATION needs to know, not just a driver

The three points above are for whoever writes a driver. If you are adopting the marketplace in an app, the part that costs money is different:

  • Charge through RoutedPayment, not through the rails. It is the recommended path and not an enforced one, because nothing in this package calls the payment verbs — only your application knows when a sale happens. Going through the rails directly skips three things at once: the routed-charge row, the receiving gate and the tax-standing gate. The row is the one that bites quietly, because the reversal cap, the merchant's earnings total and the small-business threshold verdict are all computed from it — and their readers do not fail when it is missing, they answer zero.
  • The tax-standing hold arrives with a date, and it refuses everybody until you set one. A merchant nobody has declared for is Unclarified, which is the standing that blocks — so billing.marketplace.tax_status_hold.enforce_from starts null and nothing is refused. Pick a date far enough out to collect declarations from the merchants you already have, tell them, and let it arrive. billing:marketplace:preflight reports an unset date as outstanding rather than as configured.

The mechanics are in the marketplace overview; the byte-identical single-seller guarantee is stated there once and holds for every marketplace release.


← Back to the documentation index