Skip to main content

Choosing your mode

This package runs in four modes. Which one you are in is decided by your business model, not by a preference — and three of the four stay locked until you deliberately turn them on.

Read the three questions below, take the row they land you on, and set the keys it names. Every key on this page exists verbatim in config/billing.php.

Three questions

1 · Do other people sell through your app? No → you are in Mode S. Stop here; there is nothing to configure. Yes → continue.

2 · Is what they sell content, or physical goods? Content (downloads, subscriptions, streams, one-to-one sessions) → Mode K. Physical goods sold between your users → Mode V.

3 · Are you outside Germany? This question is orthogonal to the first two. It does not choose between S, K and V — it chooses which jurisdiction's rules are loaded on top of whichever of them you are in. Answering yes puts you in Mode X, which you can be in while being in S, K or V.

The four modes

ModeWho runs itWhat it means for you
S · single seller (default)any ordinary SaaS appnone of this exists. With billing.marketplace.enabled = false the marketplace paths are unreachable and behavior is byte-identical to a build without them
K · marketplace, commission chaincontent platformsthe platform is the seller toward the buyer; sellers are settled by self-billed document, not by an invoice they send you
V · marketplace, intermediationgoods marketplaces between usersyou charge a real commission and invoice it; the goods themselves never touch your revenue
X · another jurisdictionany operator outside the shipped profilethe German rules are a profile you replace, not the package's core

Mode S — nothing to do

billing.marketplace.enabled defaults to false. Nothing on this page applies to you, no marketplace table is written, and no marketplace code runs. This is stated explicitly because the rest of the page is long and it would otherwise be reasonable to assume some of it is required setup. It is not.

The three axes, and why they are not one

This is the single most common confusion in this area, so it is worth being blunt: the mode letters, the seller's tax standing, and the payment account's state are three independent axes. Knowing one tells you nothing about the other two.

AxisThe questionValues
1 · PostureWho is the seller toward the buyer?platform_deemed_supplier · platform_intermediary · seller_of_record
2 · Seller tax standingHow is the person you pay out to treated on the incoming side?relieved by size · standard-rated · private · business elsewhere · unclarified
3 · Provider capabilityMay money move through this account at all?charges enabled · payouts enabled · deauthorized · negative balance

Two examples, each showing one axis failing to determine another:

  • A fully verified seller whose tax standing is unclarified. Axis 3 is entirely green — the account can take charges and pay out. Axis 2 is not: the package holds the settlement and issues no document, because a document stating the wrong tax is worse than a late payout. Capability said yes; standing said wait.
  • platform_deemed_supplier under a non-German profile. Axis 1 is unchanged — the platform is still the seller toward the buyer, which is a fact about your contracts, not about German law. What changes with the profile is the rate, the document wording and the reporting, none of which is the posture.

Mode X sits across all three. A French operator can run any posture; what makes them different from a German one is only the loaded profile.

What each posture produces

PostureBuyer's document namesSeller is settled byCharge type permitted
platform_deemed_supplierthe platforma self-billed documentseparate transfers
platform_intermediarythe seller for the goods; the platform for its own feea real commission invoicedestination charge or separate transfers
seller_of_recordthe seller, with their name and addresstheir own arrangementdestination charge

The charge-type column is enforced, not advisory. billing.marketplace.charge_type_by_posture maps each charge type to the postures it may carry, and a combination outside that map is refused before any money moves — not at document time, when the payment has already happened.

Note what the map does not contain: a destination charge does not permit platform_deemed_supplier. If the platform is the seller toward the buyer, money routed straight to the merchant contradicts the documents you are about to issue, and the mismatch would only surface in an audit.

Configuration per mode

Mode S

// config/billing.php
'marketplace' => [
'enabled' => false, // the default
],

Mode K — the platform is the seller

'marketplace' => [
'enabled' => true,

'seller_of_record' => [
'default_posture' => 'platform_deemed_supplier',
'allowed_postures' => ['platform_deemed_supplier'],
'supplies_are_electronic' => true,
],
],

allowed_postures is a whitelist, and it ships holding exactly one entry. A posture absent from it is refused even if something asks for it by name — which is what makes this a decision you make once in configuration rather than one that varies per request.

Mode V — you arrange, you do not sell

'marketplace' => [
'enabled' => true,

'seller_of_record' => [
'default_posture' => 'platform_intermediary',
'allowed_postures' => ['platform_intermediary'],
'supplies_are_electronic' => false,
],

'buyer_fee' => [
'enabled' => true,
'model' => 'percent', // or 'fixed'
],
],

The buyer fee is a supply of yours to the buyer, separate from the seller's fee, with its own place of supply, its own document line and its own revenue account. It is off by default.

Mode X — your jurisdiction, not ours

'tax_profile' => 'de', // the shipped profile; null means none is loaded

billing.tax_profile is a sibling of billing.tax, never a key underneath it. billing.tax is a scalar ('none' | 'eu_oss' | 'provider' | 'stripe'), and placing a key under it would turn it into an array — after which the tax calculator falls back to charging nothing and the support guard bails out on "not a string". The result is zero tax on every invoice with no error anywhere, which is why the two are kept apart.

To supply your own jurisdiction, bind your own JurisdictionProfile in the container — the config key takes only a profile the package ships, and refuses an unknown one rather than half-applying it.

Binding the profile is the first half. The second is the four jurisdiction readings — what a product is, what a reporting regime asks for, whether a self-billed document may state tax, and how a right of withdrawal reads — each of which is a separate contract still bound to the shipped country. Leave them and the install answers some questions as one jurisdiction and the rest as another, which looks completely valid from outside. php artisan billing:doctor lists any you have not replaced. See the contract reference for the full list of seams.

Why seller_of_record is closed for content

The third posture — the merchant sells to the buyer under their own name — is not reachable for electronic supplies in the shipped German profile, and that is deliberate.

For an electronic supply, the resolver requires all four of these to be true before it will hand back seller_of_record:

'seller_of_record' => [
'art9a_rebuttal_asserted' => false,
'no_agb_control' => false,
'no_billing_authorization' => false,
'no_supply_authorization' => false,
],

Three of them ship as hard false, and the fourth defaults to false. Together they say: this platform does not set the terms, does not authorize the charge, and does not authorize the delivery. A platform that publishes its own terms of service cannot truthfully assert them. Anything short of all four raises PostureNotPermitted rather than quietly falling back — a silent fallback here would put the wrong party's name on every buyer's document.

No example on this page shows that configuration succeeding, because it does not succeed. If you believe your setup genuinely satisfies all four, that is a question for your adviser before it is a question for this file.

What the buyer sees, and what they do not

The buyer's document carries only what its tier requires — which for most sales is less than a full invoice.

SaleDocumentRecipient's details
domestic, up to the small-amount thresholdsimplified receipt: your details, the date, what was sold, the gross with its rate as one sumnone
above the threshold, or cross-borderplain payment recordnone
any sale, on requestfull invoicename and address, collected at the request

The threshold is billing.marketplace.receipts.small_amount_threshold_minor.

Two consequences worth stating plainly:

  • The details do not exist before the request. A purchase completes without a name or an address ever being collected — the order carries no identity columns, and the receipt's recipient side is empty rather than blank-filled. Collecting the fields and then hiding them would be collection without a ground wearing the costume of data minimization.
  • Asking afterward does not change what you already issued. A buyer who took a receipt and later asks for an invoice gets a real document with its own number stating the same sale; the receipt they hold is untouched. Reaching back to alter an issued document is exactly what a numbered series exists to prevent.

Retention differs by what the record is for: documents are kept on the book-keeping window, and the evidence of where a sale was taxed is kept longer, on its own window. They come from different obligations, and merging them would prune the evidence before the return it justifies stops being examinable. See retention and erasure.

Goods sold between your users

In Mode V the money is passing through you, and two things follow from that.

The goods are never your revenue. They are booked as a transit item, never to a revenue account. This is enforced rather than documented: the accounts that hold revenue commonly apply a tax rate by themselves, so booking the goods there would not merely misclassify the money — it would invent tax on it, against a platform that earned a few percent of the amount.

Delayed payout is a buyer-protection feature, and the package never holds the buyer's money. Funds stay with the payment provider throughout; what is delayed is the transfer to the seller.

It is off by default (billing.marketplace.buyer_protection.enabled, or BILLING_BUYER_PROTECTION), and off means the seller's share moves the moment the payment succeeds, which is what this package has always done. Turning it on decides when a seller is paid — not a call a library makes for you. It applies to the separate_transfer lane only: on a destination charge the provider moves the money as the payment settles, so there is no moment in between for anything to hold.

With it on, a sale opens a hold instead of transferring. The hold ends one of three ways, and each announces itself as a domain event you can listen to: the buyer confirms or the confirmation window lapses (BuyerProtectionHoldReleased, and the transfer is instructed then), the sale is refunded (BuyerProtectionHoldRefunded), or a disputed hold passes its decision deadline with nobody having decided (BuyerProtectionResolutionRequired). The last one is deliberate: releasing on silence would pay a seller over an open complaint, and refunding on silence would take money from a seller who may have done nothing wrong. Both are somebody's loss, so the platform is told instead. That is why the feature is named for what it does — buyer protection with delayed payout — rather than for a custody arrangement it does not create. The naming is not a preference: a word implying you hold funds on someone's behalf describes a regulated activity, and describing yourself as doing it is a claim regulators read. Whether your particular arrangement needs a payment-services or e-money authorization is a question for your counsel — see security for where that line sits.

Decided, and still open

Decided — you do not need to ask about these. Who is the seller toward the buyer in each mode; whether an electronic supply changes that (it does not — it changes where the supply is placed); and whether a seller relieved by size earns more (they do not: the seller's net and your margin are the same in every tax standing, which is what makes crossing a relief threshold a paperwork event rather than a pay cut).

Still open, and marked as open in the code rather than guessed. The treatment of a margin-taxed resale where the platform is the deemed supplier of goods; and the correction path for a wrongly stated tax on such a document. Both are adviser questions. The package builds what is settled and refuses to decide the rest silently.