Configuration reference
Every key the package publishes, its environment variable and its shipped default. The tables follow the
order of the published files, so you can read this beside your own config/billing.php.
billing:install publishes all three files; vendor:publish --tag=billing-config re-publishes them. A key
you never touch keeps the default listed here — the package merges its own config underneath yours, so a
published file missing a key is not an error.
A key with an environment variable can be set from .env or edited in the published file. A key without
one is edited in the file, because its value is a structure rather than a scalar. In .env, a switch reads
true/false and 1/0 alike, and a number reads as the number it names.
Set
billing.customer.model. It is the one key with no useful default: without it no subscription webhook can resolve its owner. See Choosing your setup.
config/billing.php
Master switch and driver
| Key | Env | Default | What it does |
|---|---|---|---|
billing.enabled | BILLING_ENABLED | true | The master switch, off with false or 0. Off, the manager resolves the no-op NullDriver, no account routes are registered and no billing surface exists — a clone without billing boots and schedules cleanly. Usage is still metered for the application's quotas, and none of it is queued for a provider. Nothing is scheduled while it is off, so an application that meters usage schedules billing:usage:flush itself, which then only hands back expired holds. |
billing.mode | BILLING_MODE | full | What the package runs while it is enabled. full is the billing of a SaaS application. payments keeps only what takes payments into payment accounts: the two tables of the webhook deliveries and the runs of their effects, no account hub, no admin console, no Cashier routes, no default effects, and of the scheduled runs only billing:prune, which keeps the webhook records within their retention. Any other value is refused at boot. |
billing.default | BILLING_DRIVER | stripe | The payment driver used when none is named. One install runs one driver. Changing providers is a migration of this value, not a mixed mode: the driver seams a screen resolves — webhook verifier and mapper, payment methods, invoices, subscription actions, proration — are bound once at boot by the active driver, so every screen gets that driver's implementation whatever row it renders. A marketplace does not need per-owner drivers: its sellers are merchants at the same provider, scoped by MerchantScope, charged through the platform's driver against their own connected account. An application that charges into the accounts of many shops hands each account to BillingManager::forAccount() instead, see Payment accounts. |
billing.stripe.api_version | BILLING_STRIPE_API_VERSION | null | The dated Stripe API version the package sends on every call. Null uses the version the package pins and is tested against. A webhook payload is rendered in the version of the endpoint that receives it, a setting at Stripe, and billing:doctor checks every endpoint against this value where Stripe is the active driver. Override it only deliberately: Stripe versions the shape of a webhook payload, so a newer version can silently change what a mapper reads. |
billing.stripe.return_url | BILLING_STRIPE_RETURN_URL | null | Where Stripe sends the customer after a payment step that leaves the page, for a charge confirmed on-session: 3-D Secure outside Stripe.js, PayPal, Klarna. Stripe requires one for such a confirmation since its automatic payment methods became the default. Null rules redirecting methods out of the charge, so a card that needs no redirect confirms and one that does comes back as requires-action. |
billing.adyen.hmac_key | BILLING_ADYEN_HMAC_KEY | null | The HMAC key from the Adyen notification configuration, read only when billing.default is adyen. Required there, and its absence refuses the whole batch rather than skipping verification. Adyen differs from the other two drivers in a way that decides the shape: the signature is not a request header over the whole body but one field inside each NotificationRequestItem, so a batch carries as many signatures as items. Every item is checked, and one bad item refuses the request — accepting the rest would mean a caller who gets one item right decides what else travels with it. |
billing.adyen.notification_user | BILLING_ADYEN_NOTIFICATION_USER | null | The basic-auth user Adyen sends with a notification, when the account is configured to send one. Optional: left null, basic auth is not required and the HMAC alone authenticates. Set it, and a request without matching credentials is refused before any signature is read — which is the cheap check and the one that costs an attacker a round trip. |
billing.adyen.notification_password | BILLING_ADYEN_NOTIFICATION_PASSWORD | null | The password half of the above. Compared with hash_equals, and both halves are compared even when the first already failed — short-circuiting on the user name would make the response time an oracle for whether a guessed user exists. Setting one half without the other is a configuration this package treats as basic auth being on, so the missing half can never match. |
billing.mollie.api_key | BILLING_MOLLIE_API_KEY | null | The Mollie API key, read only when billing.default is mollie. Required there: the driver refuses to build a client without one, and a blank value counts as missing — a set-but-empty variable is what a half-finished deployment leaves behind and reads as configured to anybody looking at the file. A key that is present but malformed is refused with the same setting named, rather than letting the SDK's own exception through with a stack trace from a third-party library. |
billing.mollie.webhook_url | BILLING_MOLLIE_WEBHOOK_URL | null | Where Mollie posts its status pings. A customer returns to the billing.checkout.* URLs instead. Must be absolute and reachable from the internet. It is configuration rather than a generated route URL because neither thing route() needs is reliably present: a package cannot know the public host, and the scheduled billing run creates payments from the CLI, where there is no request to take a host from. Left null it falls back to app.url joined with billing.webhook_path — right for a single-host install, wrong behind a development tunnel, where it must be the tunnel's address or Mollie posts to a host it cannot reach. |
billing.mollie.methods | — | null | The payment methods this Mollie account offers at checkout. Configurable because Mollie enables methods per account, so a fixed list would be wrong for most installs in both directions at once — offering what the account cannot take and hiding what it can. Left unset, the driver falls back to the methods a mandate can exist for, rather than to an empty list: empty reads as "this account can take no payments" and every screen that asks would render nothing. |
billing.mollie.webhook_secret | BILLING_MOLLIE_WEBHOOK_SECRET | null | The signing secret for Mollie's next-generation webhooks, which carry an HMAC-SHA256 in X-Mollie-Signature. Accepts a list as well as a single string, because rotation is a period where both secrets are live — without that an operator has to choose between rotating and losing webhooks, which is not a choice but a reason not to rotate. Left null, nothing is checked: every ping is authenticated by the fetch, since an attacker cannot invent a status Mollie will confirm. Set it, and a request that carries a signature has to carry a valid one, and an unsigned request shaped like a next-generation event (a type or an entityId) is refused, because that generation signs every delivery. The classic ping Mollie sends for each payment, which names the payment's id and nothing else, is never signed, whatever the dashboard says, so it is accepted either way and authenticated by the fetch. |
billing.mollie.terminal_countries | — | [] | Where each point-of-sale terminal stands, as an ISO 3166 country code. A sale at the counter is taxed in the country it is made in, and Mollie reports no address for a terminal, so the country is yours to state. Key an entry by a terminal's id (term_…) or by a profile's id (pfl_…) for every terminal of that profile; a terminal's own entry wins, even when it is unusable. A terminal with no usable entry takes no sale. See Payment in person. |
billing.mollie.account_http.request.timeout | BILLING_MOLLIE_ACCOUNT_REQUEST_TIMEOUT | 10 | Seconds until a request of a payment account's client gives up inside a web request. |
billing.mollie.account_http.request.connect_timeout | BILLING_MOLLIE_ACCOUNT_REQUEST_CONNECT_TIMEOUT | 3 | Seconds until its connection gives up inside a web request. |
billing.mollie.account_http.request.retries | BILLING_MOLLIE_ACCOUNT_REQUEST_RETRIES | 0 | How often a request that never reached Mollie is sent again inside a web request. With 0 a caller hears at once that Mollie cannot answer. |
billing.mollie.account_http.request.retry_delay_ms | — | 0 | The pause before the first such retry, in milliseconds, growing by the same step before each further one. |
billing.mollie.account_http.background.timeout | BILLING_MOLLIE_ACCOUNT_BACKGROUND_TIMEOUT | null | Seconds until a request of a payment account's client gives up in a queue worker, the scheduler or Artisan. Null keeps the application's own HTTP default. |
billing.mollie.account_http.background.connect_timeout | BILLING_MOLLIE_ACCOUNT_BACKGROUND_CONNECT_TIMEOUT | null | Seconds until its connection gives up there. Null keeps the application's own HTTP default. |
billing.mollie.account_http.background.retries | BILLING_MOLLIE_ACCOUNT_BACKGROUND_RETRIES | 5 | How often a request that never reached Mollie is sent again there, the SDK's own number. Every retry carries the request's idempotency key. |
billing.mollie.account_http.background.retry_delay_ms | — | 1000 | The pause before the first such retry, in milliseconds, growing by the same step before each further one. |
Webhooks
| Key | Env | Default | What it does |
|---|---|---|---|
billing.webhook_path | BILLING_WEBHOOK_PATH | billing/webhook | The path the provider posts to. The route carries no middleware group and no CSRF — the driver's verifier authenticates by signature instead. |
billing.webhook_middleware | — | [] | Middleware added to every webhook endpoint: the install's, the marketplace's and the address of each payment account. The package reads no network address, so it ships no throttle per address; name one here if you want it, for instance throttle:billing-webhooks with a limiter your application defines. Stripe and Mollie answer a 429 by sending the delivery again later. |
billing.webhooks.connection | BILLING_WEBHOOK_QUEUE_CONNECTION | null | Queue connection for webhook effects. Null uses the default connection. |
billing.webhooks.queue | BILLING_WEBHOOK_QUEUE | null | Queue name for webhook effects. Set it to keep billing work off the queue your app's other jobs share. |
billing.webhooks.tries | BILLING_WEBHOOK_TRIES | 5 | How often a failing effect is retried before the job is marked failed. It stays re-driveable after that — the raw payload is stored, so billing:webhooks:replay --failed can run it long after the provider stopped redelivering. |
Each effect runs in its own queued job, so a slow or failing effect can neither hold the provider's request open nor take the other effects down with it. See Webhooks.
Checkout
| Key | Env | Default | What it does |
|---|---|---|---|
billing.checkout.success_url | BILLING_CHECKOUT_SUCCESS_URL | null | Where a hosted checkout returns on success. Null uses the hub's checkout-return route for a subscription, which reconciles the subscription onto the local row so a paying customer is never shown the free tier, and the add-ons on the plan screen for an add-on purchase and for an on-session PaymentRails::charge() on Mollie. A tip checkout on Stripe needs it set. |
billing.checkout.cancel_url | BILLING_CHECKOUT_CANCEL_URL | null | Where an abandoned checkout returns. Null uses the plan screen. A tip checkout on Stripe needs it set. |
billing.checkout.purchase_success_url | BILLING_CHECKOUT_PURCHASE_SUCCESS_URL | null | Where a completed add-on purchase returns, and an on-session PaymentRails::charge() on Mollie, for an application that sends a subscription and a one-time purchase to different pages. Null uses success_url, then the add-ons on the plan screen. |
billing.checkout.purchase_cancel_url | BILLING_CHECKOUT_PURCHASE_CANCEL_URL | null | Where an abandoned add-on purchase returns. Null uses cancel_url, then the plan screen. |
billing.checkout.portal_return_url | BILLING_PORTAL_RETURN_URL | null | Where the hosted billing portal returns, unless the caller of HostedPortal::url() names its own. Falls back to the subscription screen, never to success_url. |
billing.checkout.payment_methods_return_url | BILLING_PAYMENT_METHODS_RETURN_URL | null | Where a hosted "add a card" page returns. Falls back to the payment-methods screen. |
billing.tax_counters.reversal_attribution | BILLING_TAX_COUNTER_REVERSAL_ATTRIBUTION | original_period | Which window a reversal reduces: the one it corrects, or the one it happened in (reversal_period). One key governs both counters, so the threshold figure and the reported figure always agree about WHERE a reversal belongs — they still differ about its size. Only safe together with the rule that a completed threshold crossing is final; see the notes above the key in the published config. |
billing.tax_counters.dac7 | — | (section) | The reporting counter's own settings. |
billing.tax_counters.dac7.enabled | BILLING_TAX_COUNTER_DAC7_ENABLED | true | Whether this installation carries the reporting gross-inflow counter at all. Turn it off on a platform outside the regime. Off refuses rather than answering zero — a zero is itself a reportable figure, and a return built on one would state that every seller received nothing, with nothing red anywhere. It gates the reporting basis only: the section 19 threshold counter answers a different question and keeps running. |
billing.marketplace.reporting | — | (section) | Where a produced seller-reporting record is copied to. |
billing.marketplace.reporting.export_disk | BILLING_REPORTING_EXPORT_DISK | null | The disk a copy of a produced reporting record is written to. Null is a supported answer, not a missing setting: the record itself keeps the exact bytes, so an operator whose accounting reads them off the record needs no second copy — and writing one anyway would put a document containing sellers' figures somewhere nobody asked for it. |
billing.marketplace.reporting.export_path | BILLING_REPORTING_EXPORT_PATH | reporting | The directory on that disk. Files are named <year>-<format>-<currency>.txt, year first, so a directory sorts into filing order rather than alphabetically by format. |
billing.tax_counters.us_state_gmv | — | (section) | The subdivision gross-sales counter's own settings. |
billing.tax_counters.us_state_gmv.enabled | BILLING_TAX_COUNTER_US_STATE_GMV | false | Whether this installation counts buyer gross per subdivision of a destination country — the early warning for an obligation reached per state rather than nationally. Off by default, unlike the reporting counter: that one is on because it is what the package has always done, while most installations have no subdivision-level obligation anywhere. Independent of dac7.enabled in both directions, and independent of any geoblock — a counter that started when a market opened would produce its first useful figure after the year that could have breached. Off refuses rather than answering zero, because zero here reads as "we sold nothing into that state". |
billing.checkout.promotion_codes | BILLING_CHECKOUT_PROMOTION_CODES | true | Whether the provider's promotion-code field is offered at checkout. |
billing.checkout.tax_id_collection | BILLING_CHECKOUT_TAX_ID_COLLECTION | true | Whether a checkout taxed by the provider asks the buyer for a tax ID. Stripe reverse-charges on the ID's format, not its validity: it verifies the ID only after the session, so a well-formed invalid ID buys without VAT. A platform that sells to consumers can set it to false and charge every buyer their country's tax. It is the default for every checkout: subscribe(…, collectTaxId: true) and purchase(…, collectTaxId: true) ask on one checkout where it is false, and false leaves the field out where it is true. Read only while the provider computes tax. |
Owner model and seats
| Key | Env | Default | What it does |
|---|---|---|---|
billing.owner | BILLING_OWNER | user | Who owns billing: user (each user pays for themselves) or team (the user's team pays, for seats). |
billing.team_relation | — | team | With a team owner, the relation on the acting user that returns the paying team. Ignored for a user owner. Point it at a relation that is the membership — see the note below. |
billing.seats.membership_relation | — | members | The relation on the team model that returns its members. HasSeats counts it. |
billing.seats.active_status_column | — | null | Column to filter the member count to active members. Null when the relation already returns only active ones — a pending invite is not a paid seat. |
billing.seats.active_status_value | — | active | The value that column must hold to count. |
billing.seats.membership_events | — | [] | Your join, leave and remove events. A queued listener is registered on each, so a membership change re-syncs the billed quantity. Empty means nothing fires until you opt in. |
billing.seats.owner_properties | — | ['team', 'owner'] | For an event that does not implement AffectsSeats, the first of these properties holding the team model is read. |
The owner model receives the billing notices
The shipped notifier sends every billing notice to the billing owner through Laravel's notification stack: a
failed payment, a suspension warning, a receipt, a trial about to end. The mail channel asks the owner for its
address with routeNotificationFor(), and the database channel asks it for its notifications() relation.
Laravel's Notifiable trait provides both, so the owner model needs it, or those methods of its own. A team
model often has neither. Without them each notice fails in its queued job and never arrives, and
billing:doctor names every owner model in your billing records that a notice cannot reach. To send a team's
notices to someone else, its owner for example, bind notifiers of your own (see Contracts).
If team_relation points at a pointer, implement VerifiesBillingMembership
The team owner is whatever billing.team_relation returns off the acting user, and for a relation
that is the membership — a belongsTo, a belongsToMany — that answer carries its own proof:
the row exists because the user belongs to the team.
A great many applications point at a mutable column the user controls instead, current_team_id
being the usual one. There the two questions come apart: the pointer keeps answering after the
user has been removed from the team, and billing would follow it.
This package cannot tell the two apart from the outside, and probing for method names on your team model would be guesswork. So your model declares it:
use Illuminate\Database\Eloquent\Model;
use Pushery\Billing\Contracts\VerifiesBillingMembership;
class Team extends Model implements VerifiesBillingMembership
{
public function hasBillingMember(Model $actor): bool
{
return $this->hasMember($actor);
}
}
A team that implements it is asked before it is billed, and a false answer falls back to the
acting user — the same direction the resolver already takes when team mode yields no team at all,
so a removed member is neither billed through the team nor left without an owner.
A team model that does not implement it is trusted, exactly as before. This is a seam you opt into, not a behavior change you are handed.
Customer and tier resolution
| Key | Env | Default | What it does |
|---|---|---|---|
billing.customer.model | BILLING_CUSTOMER_MODEL | null | The Eloquent model that owns a provider customer record. Set it before you take a real payment. |
billing.customer.column | — | stripe_id | The column holding the provider's customer reference. |
billing.schema.host_key_type | BILLING_HOST_KEY_TYPE | null | What your models are keyed by: int, uuid or ulid. Left null it follows the framework: Schema::morphUsingUuids() or morphUsingUlids() in your application makes it uuid or ulid, and without either it is int. Every column referencing one of your models is declared with this type when its table is created, as Laravel's own uuidMorphs() and ulidMorphs() declare it: on PostgreSQL a uuid key is the native uuid type, so a table of yours keyed by $table->uuid('id') can be joined to the package's tables, which is what whereHas() compiles to. Set it before you migrate — get it wrong and the rows cannot be written at all, and changing it afterwards is a data migration rather than a settings change. billing:doctor reports a setting that disagrees with the tables, and a value that is none of the three: the migrations read such a value as int, so write it in lowercase. |
billing.zero_tier | BILLING_ZERO_TIER | free | The fail-safe no-entitlement tier every resolver falls back to. |
billing.tier_column | — | plan | The raw column ColumnTierResolver reads — never an accessor. |
billing.untouchable_tiers | — | [] | Tier keys granted by hand and kept out of the billing flow: the plan screen does not offer them, a checkout refuses them, and the plan-sync webhook effect never flips them, in either direction. Use it for an admin-comped grant a provider event must not overwrite. A tier's own untouchable: true counts the same. |
billing.order_item_preprocessors | — | [] | Steps that may reshape a billing cycle's lines before its order is written, run in the order listed. Each implements OrderItemPreprocessor and is resolved through the container. Only a driver with a local engine reaches this — a provider that meters remotely returns an amount that is already correct. Empty is the right default: what a cycle costs beyond its plan price is a question only your application can answer. A step that throws aborts that cycle before it is claimed, so it is retried rather than billed at a partial total. |
A record of what already happened finds its owner past your global scopes. Usage that was recorded, a share held for a sale, a settlement document and a webhook about a customer the provider still bills stand in the name of an owner, and a soft delete or a tenant scope on your model hides the row without settling what that owner owes or is owed. These paths look the owner up with your model's global scopes removed, as the seller reporting period does, and find nothing only where the row itself is gone. A webhook asks your own view first, so a row you show wins over a hidden one with the same customer reference.
Tiers, add-ons and coupons
| Key | Env | Default | What it does |
|---|---|---|---|
billing.tiers | — | one free tier | The tier catalog, keyed by tier key; the order is the upgrade ranking. Per tier: label, an optional provider_price (an id, or a per-driver map), price_display, interval, legacy_prices, metered components, trial, an archetype for the consumer-rights profile (unset, a tier is a subscription), buyers (business to sell the tier only to an owner IdentifiesBusinessBuyers names as a business; unset, anyone), and the presentation-only features, highlight and badge. A tier with no price_display is not purchasable. The client submits a tier key, never a price. |
billing.addons | — | [] | One-time purchasable add-ons, keyed by add-on key: label, provider_price, price_display, an optional grants (meter plus units), an optional buyers (business to sell the add-on only to an owner IdentifiesBusinessBuyers names as a business; unset, anyone), and an optional consignment for goods sold for a merchant (imported with intrinsic_value_minor in euro cents, or goods_located_in, beside new_means_of_transport, installed_or_assembled, subject_to_excise and basis, and the description and quantity a deemed supply needs), which the hosted purchase classifies a deemed supply from. An add-on grants prepaid usage units when it has grants, money credit when it has neither grants nor an archetype or has the voucher archetype, and nothing when it has any other archetype or is a work the register of works maps. |
billing.coupons | — | [] | Package-owned discount codes keyed by the code the customer enters, which matches whatever its case and the whitespace around it: either percent (1 to 100) or amount plus currency, with an optional expires_at: a date the code still resolves on, through the end of that day, or a moment with a time of day at which it stops. Add stripe_coupon to have the provider own the money math at checkout. The map carries no issuer, so a code declared here applies on every sale, a merchant's included; a code only one seller should honor is a billing_coupons row issued by that seller, and a live row answers before this map. |
billing.currency | BILLING_CURRENCY | EUR | Used where an amount carries no explicit currency, such as a zero dunning fee. |
The shape of a tier is worked through in Tiers and pricing; add-ons and prepaid units in Usage-based billing.
Usage and metering
| Key | Env | Default | What it does |
|---|---|---|---|
billing.dimensions | — | [] | Extra dimensions a custom UsageProvider reports: label, unit, period, warn_threshold. The default provider ignores this — it derives its dimensions from each tier's metered components, so an owner sees exactly what they are billed for. |
billing.metering.max_attempts | BILLING_METERING_MAX_ATTEMPTS | 8 | A deadline, not a limit: past it the usage is marked failed and logged as an error, because it is revenue that will not be collected unless someone acts. Do not raise it to hide a persistent failure. A value that is not a number falls back to 8. |
billing.metering.backoff_seconds | BILLING_METERING_BACKOFF_SECONDS | 60 | Base delay for the exponential backoff between flush attempts. |
billing.metering.stall_hours | BILLING_METERING_STALL_HOURS | 6 | How long usage may sit unreported before billing:usage:reconcile calls it a stall rather than a passing outage. Keep it under your provider's back-dated acceptance window. |
billing.metering.closed_period_grace_minutes | BILLING_METERING_CLOSED_PERIOD_GRACE_MINUTES | 60 | How long after a billing period ends its usage can still reach that period's invoice. Stripe keeps a cycle invoice as a draft for an hour by default, and an account can set up to 72 hours. Usage of an ended period that reaches the flush later than that would be counted in the provider's meter and billed on no invoice, so it is marked failed and logged as an error instead. Set it to your account's grace. A value that is not a whole number of zero or more falls back to 60. |
billing.usage.hold_seconds | BILLING_USAGE_HOLD_SECONDS | 900 | How long a hold on a metered allowance stands before billing:usage:flush hands it back. Longer than your slowest metered request, shorter than you would tolerate an owner being short of allowance they never spent. |
billing.usage.spend_cap.enabled | BILLING_USAGE_SPEND_CAP | false | Whether metered usage is measured against a spend cap per owner. See A spend cap per customer. |
billing.usage.spend_cap.default | BILLING_USAGE_SPEND_CAP_DEFAULT | 0 | The cap of an owner who never set one, in minor units of billing.currency. 0 allows no usage past the included allowance. |
billing.quota.status | — | 429 | The HTTP status the billing.quota:<meter> middleware aborts a blocked request with. Only a blocking meter is gated; a degrade or fair-use meter never is. |
Dunning, suspension and notifications
| Key | Env | Default | What it does |
|---|---|---|---|
billing.dunning | — | three rungs, at 3, 7 and 14 days | The ladder, in order. Each rung: after_days, an optional fee (amount plus currency), an optional label. after_days and the fee's amount are integers, or strings of digits as env() delivers them. billing:dunning:advance sends each rung's warning once and charges its fee if one is set. The delinquency clock is a timestamp, never a gateway status. |
billing.dunning_cure_window_days | BILLING_DUNNING_CURE_WINDOW_DAYS | 7 | How long a subscription in arrears can still be rescued. Access to that merchant is withdrawn immediately when the payment fails; this window is a chance to cure it, not a grace period with the service still running. billing:dunning:remind sends one reminder per day inside it, and the subscription expires when it runs out. It counts calendar days in the application's zone: with a window of 7, a payment that failed on the 1st is reminded through the 7th and expires on the 8th, whatever time it failed and whenever the two commands run. A sibling of the ladder above rather than a rung of it: the ladder decides which surfaces are withdrawn as time passes, this decides how long the relationship survives. Floors at one day. |
billing.dunning_status | — | 402 | The status a non-browser request from a delinquent owner gets. A browser request is redirected to the recovery screen instead. |
billing.suspension | — | [] | Per-surface lockout thresholds keyed by surface name; the value is the dunning level at which that surface locks. A surface with no threshold never locks. |
billing.cards.warn_within_days | — | 30 | How far ahead billing:cards:warn nudges an owner whose default card is expiring — the biggest preventable cause of involuntary churn. |
billing.notifications.channels | — | ['mail'] | The transport only, never whether the customer is told: billing notices are transactional and non-suppressible. Add database for an in-app feed. An unusable value falls back to mail rather than sending nothing. The ten notices to a billing owner carry a call-to-action button, and each links into the account hub unless you bind BillingActionUrls to send an owner you manage elsewhere to a screen of your own (contracts) — the trial notice to the payment-methods screen or the plan screen depending on whether a card is already on file, a failed payment and a suspension warning to recovery, a receipt to the invoice history. Those links appear only where the hub's routes are registered, which needs Livewire installed; without it the mail renders exactly as it did before, with its text and no button rather than a dead link. The tax-standing notice deliberately carries none: it reports a decision somebody else recorded and no hub screen edits it. |
billing.notifications.tries | — | 3 | How many times the queue tries a billing notice before it gives up. A mail server that turns a message away for a passing reason, a timeout or a temporary refusal, has not taken it, so another try does not deliver it twice, and the queue sends each channel as a job of its own, so a retry repeats only the channel that failed. 1 sends once and leaves a failed notice failed. A value that is not a positive whole number reads as 3. |
billing.notifications.backoff | — | [60, 300] | The seconds the queue waits before each retry of a billing notice: the first entry before the second try, the next before the third, and the last one before every try after it. A list with anything but positive whole numbers in it reads as the shipped one. |
Account hub, admin and runtime
| Key | Env | Default | What it does |
|---|---|---|---|
billing.navigation | — | the full hub | The hub's sections, keyed by item key: label (an i18n key or a literal), route, and the optional group, icon, order and web_only. Malformed items are dropped. Remove an entry and the section is gone. |
billing.runtime | BILLING_RUNTIME | web | web or native. On native, items flagged web_only are hidden — for flows an app store forbids from being completed in-app. |
billing.link_out | BILLING_LINK_OUT | null | The external billing portal to link out to when an external merchant of record owns billing. Scheme-restricted: anything but an absolute http or https URL with a host is ignored. |
billing.redirect_hosts | — | [] | Hosts the application adds to the ones its payment driver serves pages from. A checkout, the page that adds a card, the customer portal and a merchant's onboarding are handed out only over https and on such a host: checkout.stripe.com, billing.stripe.com and connect.stripe.com for Stripe, www.mollie.com for Mollie. The install's Stripe account lists its custom domain here, such as payments.example.com, or its Checkout and portal open nowhere. A payment account names its own in PaymentAccount::$redirectHosts, which only that account's checkout accepts. Each refusal is logged with the host. |
billing.provider_budget.calls | BILLING_PROVIDER_BUDGET_CALLS | 30 | How many calls to the payment provider one payer may make in each window. The account screens spend from it before every render or action that asks the provider, and a screen of your own spends from the same budget with ProviderCallBudget::spend($owner), so the two share one count. A request past it is answered 429 before the provider is asked. null switches the budget off for an application that limits these requests itself. |
billing.provider_budget.seconds | BILLING_PROVIDER_BUDGET_SECONDS | 10 | The length of that window, in seconds. |
billing.realtime.enabled | BILLING_REALTIME | false | Opt-in live refresh for the hub. Events broadcast only when this is on and a broadcaster is configured; otherwise the screens fall back to a bounded poll. |
billing.realtime.render_toast_region | BILLING_REALTIME_TOAST_REGION | false | Whether this package renders somewhere for the realtime toasts to land. The bridge dispatches a wirekit-toast browser event; something has to listen, and by default that is not this package. A WireKit host already renders a region reading exactly this event, which is why the default is off — a second one would show every toast twice, and that duplicate is visible only in a browser. Turn it on if your application has no toast region of its own: you get two aria-live containers (polite for info and success, assertive for warning and danger) and a small inline listener that appends the message and dismisses it after a few seconds, with no UI kit and no build step. The third option is neither — leave it off and write a one-line listener on wirekit-toast reading detail.message and detail.variant. Has no effect at all unless billing.realtime.enabled is also on, because the region follows the bridge. |
billing.admin.enabled | BILLING_ADMIN_ENABLED | true | Whether the admin console is mounted at all. Turn it off when your back office has billing screens of its own: the console's route and its Livewire component are then not registered. |
billing.admin.ability | BILLING_ADMIN_ABILITY | billing-admin | The Gate ability every admin-console access is authorized against. Your app defines it; until you do, the Gate denies everyone. |
billing.admin.prefix | BILLING_ADMIN_PREFIX | admin/billing | The URL prefix the admin console mounts under. |
billing.admin.middleware | — | ['web', 'auth'] | The middleware stack the admin console runs through. |
billing.admin.layout | BILLING_ADMIN_LAYOUT | billing::layouts.admin | The Blade layout the admin console renders in. The default is a page of its own; point it at your back office's layout and the console takes its place in that shell. The account hub has the same lever in account.layout. |
billing.subscriptions.warn_ending_within_days | — | 3 | How far ahead billing:subscriptions:warn reminds an owner that a subscription they canceled ends, while resuming still keeps the plan without a gap. Override per run with --days. |
billing.subscriptions.downgrade_timing | BILLING_DOWNGRADE_TIMING | period_end | When a downgrade lands. An upgrade is always immediate. period_end avoids owing a refund or taking away paid-for access mid-cycle; immediate downgrades at once, and any other value reads as period_end. Whether a change is a downgrade is the tier's rank in billing.tiers, not its price, so a tier listed higher and priced lower is an upgrade. The screen and the swap read this one value and this one ranking, so they cannot disagree. |
Trials
| Key | Env | Default | What it does |
|---|---|---|---|
billing.trial.days | BILLING_TRIAL_DAYS | 0 | Trial length in days; 0 disables trials. Per-tier override: tiers.<key>.trial.days. |
billing.trial.mode | BILLING_TRIAL_MODE | null | none, subscription (collected at checkout) or generic (no subscription, granted by Trials::grant()). Null derives it: a configured generic_tier implies generic, otherwise a positive length implies subscription. |
billing.trial.generic_tier | BILLING_TRIAL_GENERIC_TIER | null | The tier a generic trial unlocks. Null disables generic trials — without a tier to unlock there is nothing to grant. |
billing.trial.requires_payment_method | BILLING_TRIAL_REQUIRES_PM | true | Whether a subscription trial collects a card up front. |
billing.trial.ending_within_days | — | 3 | How many days before a trial ends the app-shell banner starts nudging. |
Tax and the invoice seller
| Key | Env | Default | What it does |
|---|---|---|---|
billing.tax | BILLING_TAX | none | provider (defer to a provider that supports it), eu_oss (the bundled static EU-OSS VAT table) or none. A driver-capability decision, not a checkout option. |
billing.tax_matrix | — | null | Rates keyed by destination country and supply category, for jurisdictions that tax some supplies at a reduced rate: an array with a valid_from date and a rates map of country => category => basis points (1900 = 19%). Null means the bundled standard-rate table answers alone, exactly as before. A country in the matrix with no reduced band charges its standard rate — an absence there means "not granted here", which is an answer — while a country the matrix omits is still priced by the bundled table, so a partial matrix is a smaller table rather than a smaller world. Any audio or video part of a supply closes the reduced band for the whole of it, with no majority test and no threshold; splitting the product is how a reduced band is reached. A malformed value is refused at build rather than ignored, because a wrong rate looks exactly like a right one on an invoice. Country codes may be written in either case; a band that is not a rate category, a rate that is not a whole number and two spellings of one country count as malformed. A sibling of billing.tax, never a child — nesting under that scalar would turn it into an array and silently produce 0% tax on every invoice. An optional max_age_days entry inside it (default 180) sets how old the table may be before billing:doctor calls it out and exits non-zero — a diagnostic rather than a boot guard, because refusing to boot over an aged table takes an application down for something still mostly right. Leave this null and an active billing.tax_profile supplies its own country's rates, so an operator in a shipped jurisdiction does not hand-type them — a hand-typed rate looks exactly like a right one on an invoice. A value here wins over the profile, because an operator who priced their own table has a reason the package cannot know. It may also carry billing.tax_matrix.max_age_days (default 180) — how many days old the table may get before billing:doctor reports it. That sub-key is not settable on its own: it lives inside the matrix, because how long a table may sit depends on the table, and setting it alone would turn tax_matrix from null into an array and change which rates answer. An optional history entry holds the same rates as dated intervals — a list of valid_from / valid_to / source / source_version / fetched_at / rates, plus an optional approved_by — so a document written today about a supply taxed earlier can be priced at the rate that applied then, which is what the law binds it to. Absent, nothing changes and a tax point is simply ignored. Present, a country the history carries is answered from it and a tax point falling in a gap is refused rather than answered with the nearest rate, because an invented rate with a date on it cannot be told apart from a real one; a country the history omits keeps being priced by the table above. Intervals are append-only and an overlapping pair is refused, since one supply cannot have two rates. |
billing.tax_oss | — | the three keys below | Whether the small-turnover threshold for cross-border consumer sales applies to you. A sibling of billing.tax, never a child — that key is a scalar, and an array there resolves to no tax mode at all. |
billing.tax_oss.threshold_waived | BILLING_TAX_OSS_THRESHOLD_WAIVED | true | The default does not mean the package declared anything on your behalf: it means no origin-country fallback is applied, which is the direction that never under-charges. Charging your own rate where the buyer's was owed under-declares in a country nobody is registered in and surfaces as an assessment years later. Turn it off and bind a CrossBorderSalesCounter to have the threshold watched instead. Watched, a cross-border sale to a consumer that the package bills itself is taxed at the seller's rate while the year stays below the profile's limit, and the sale that takes it past the limit is taxed at the destination; the document keeps the customer's country, so the sale counts toward the limit. |
billing.tax_oss.waived_since | BILLING_TAX_OSS_WAIVED_SINCE | null | When you actually declared it. Withdrawing the declaration inside its binding period is refused at boot — it would contradict a filing the revenue office holds, and silently reverting to origin taxation would under-declare in every destination country until somebody noticed. |
billing.tax_oss.warning_levels | — | [0.80, 0.95] | Fractions of the threshold at which you want to hear about it, so a registration does not begin on the day it is already needed. |
billing.tax_profile | BILLING_TAX_PROFILE | null | Which country's obligations the package enforces where it can — today, the points a jurisdiction adds to the marketplace go-live checklist. Null means none, and the checklist says so rather than reading as an all-clear. Shipped: de. A jurisdiction the package does not ship is supplied by binding your own JurisdictionProfile, which wins over this name; a name that is neither shipped nor bound is refused at boot. |
billing.tax_small_business | — | the keys below | Thresholds a jurisdiction profile with a small-business rule reads. Inert without such a profile. |
billing.tax_small_business.warning_levels | — | [0.80, 0.95] | How close to a threshold triggers a forced re-declaration. The percentages are configurable; the basis they are computed on is not — the same percentages against a different basis fire at a completely different moment, which would look like a configuration choice and behave like a different rule. |
billing.tax_small_business.eu_revalidate_after_days | BILLING_TAX_EU_REVALIDATE_AFTER_DAYS | 365 | How long a confirmed small-business registration stands before it is checked again. A registration confirmed once is not confirmed forever — registers change, and a standing resting on a two-year-old lookup rests on nothing. Once it lapses the merchant is held exactly as if nobody had ever checked. |
billing.tax_small_business.reattestation.grace_days | BILLING_TAX_REATTEST_GRACE_DAYS | 30 | How long somebody has to answer an obligation that arrives on the first day of the year. Added to the boundary, never replacing it. |
billing.tax_small_business.reattestation.remind_days | BILLING_TAX_REATTEST_REMIND_DAYS | 14 | How many days before an attestation runs out its creator is reminded, on top of the notice when the renewal falls due at the year boundary. |
billing.tax_small_business.previous_year_limit | BILLING_TAX_SB_PREVIOUS_YEAR_LIMIT | 2500000 | Prior-year turnover ceiling, in minor units (the German §19 figure: €25,000). A count over it last year removes the exemption for the whole of this year, effective January 1. |
billing.tax_small_business.current_year_limit | BILLING_TAX_SB_CURRENT_YEAR_LIMIT | 10000000 | Running-year turnover ceiling, in minor units (€100,000). The transaction that breaks it ends the exemption from that transaction's own moment — not the day's start, not the year's. |
billing.tax_small_business.founding_year_limit | BILLING_TAX_SB_FOUNDING_YEAR_LIMIT | 2500000 | Turnover ceiling that applies in the founding year, in minor units (€25,000). A business in its first year has no prior year, so the current-year limit is replaced by this immediate one. |
billing.tax_small_business.auto_flip_enabled | BILLING_TAX_SB_AUTO_FLIP | true | Whether a broken limit automatically flips the creator from small-business to standard rating. The flip is one-directional by design — a count under the limit never flips back, because the platform sees only a lower bound of the real turnover. Turning this off leaves the flip to an operator. |
billing.company.name | BILLING_COMPANY_NAME | null | The seller party on an e-invoice. |
billing.company.vat_id | BILLING_COMPANY_VAT_ID | null | Your VAT identification number. Also the last fallback for the electronic address below. |
billing.company.address | BILLING_COMPANY_ADDRESS | null | Street address of the seller party. |
billing.company.postcode | BILLING_COMPANY_POSTCODE | null | Postal code of the seller party. |
billing.company.city | BILLING_COMPANY_CITY | null | City of the seller party. |
billing.company.country | BILLING_COMPANY_COUNTRY | DE | Two-letter country code of the seller party. |
billing.company.endpoint_id | BILLING_COMPANY_ENDPOINT_ID | null | The seller electronic address (EN 16931 BT-34). XRechnung makes it mandatory, so an endpoint must always resolve: set it, or the renderer falls back to a company email, then to vat_id. |
billing.company.endpoint_scheme | BILLING_COMPANY_ENDPOINT_SCHEME | EM | The scheme code for that address (EM is email). |
billing.company.contact_name | BILLING_COMPANY_CONTACT_NAME | null | The seller contact point (BT-41), such as a department. XRechnung requires it on every document, with the two below. |
billing.company.contact_phone | BILLING_COMPANY_CONTACT_PHONE | null | The seller contact's telephone number (BT-42). |
billing.company.contact_email | BILLING_COMPANY_CONTACT_EMAIL | null | The seller contact's email address (BT-43). |
billing.company.iban | BILLING_COMPANY_IBAN | null | The account a document asks to be paid on by SEPA credit transfer (BT-84). XRechnung requires payment instructions, and they are written only when this is set. |
billing.company.bic | BILLING_COMPANY_BIC | null | The bank of that account (BT-86). |
Tax and the invoice surface: Taxes · Invoices and e-invoicing.
SEPA direct debit
| Key | Env | Default | What it does |
|---|---|---|---|
billing.direct_debit.notice_days | BILLING_DIRECT_DEBIT_NOTICE_DAYS | 14 | How many calendar days before a direct debit the payer is told its amount and its day. The SEPA core rulebook sets 14 where nothing else is agreed. The mandate text this package shows agrees the number set here, and the consent keeps it, so a mandate given on your form is announced by the period it was given with. A mandate a first payment established is announced by this number too: iDEAL, Bancontact and the other bank methods are collected as direct debits afterwards, and a shorter period has to be agreed in your terms for them. A value that is not a whole number of at least 1 reads as 14. |
billing.direct_debit.mandate_wordings | — | AT, DE, LI and LU read the German wording | The mandate text for payers in each country, keyed by the ISO country code. The rulebook asks for a language of the payer's country, and a country without an entry gets no mandate on your form. Another language is a class of your own implementing SepaMandateWording. |
The mandates, the text and the notices: Subscriptions.
Data protection and retention
| Key | Env | Default | What it does |
|---|---|---|---|
billing.audit.level | BILLING_AUDIT_LEVEL | money | money records every money movement and entitlement or state change — the events an auditor, or a "why is this customer on free?" question, needs. all adds the high-volume navigational and read-side events. |
billing.erasure.forget_customer | BILLING_ERASURE_FORGET_CUSTOMER | false | Whether billing:erase also deletes the customer at the provider. Irreversible, and it cancels their live subscriptions there — off by default. |
billing.erasure.merchant_subscriptions | BILLING_ERASURE_MERCHANT_SUBSCRIPTIONS | now | How the subscriptions fans hold with a merchant whose account is deleted end: now ends them immediately, period_end lets each run to the end of the period the fan paid for and renew no more. Any other value reads as now. |
billing.retention.webhook_payload_days | BILLING_RETENTION_WEBHOOK_PAYLOAD_DAYS | 90 | How long a stored webhook payload is kept. Long past the provider's own redelivery window, which is the only reason it is kept at all. |
billing.retention.erased_financial_days | BILLING_RETENTION_ERASED_FINANCIAL_DAYS | 2920 | The invoice window for an erased owner's retained invoices: eight years, counted from the end of the year of issue. A shorter value refuses to boot. A floor and a default — set your own for another jurisdiction. |
billing.retention.audit_days | BILLING_RETENTION_AUDIT_DAYS | 3650 | The book window for the audit ledger: ten years. Deliberately longer than the invoice window above — two record classes, not a value that drifted. Do not unify them. The produced tax returns and seller-reporting records keep this window as well, counted from the end of the year a record was produced in. The filings of a reporting period keep it too, and go together once the youngest of them has had it. |
billing.retention.allow_below_statutory_minimum | BILLING_RETENTION_ALLOW_BELOW_STATUTORY_MINIMUM | false | The escape hatch for a jurisdiction whose invoice minimum genuinely is shorter than the floor above. Left false, a shorter erased_financial_days refuses to boot rather than prune tax records early. |
Erasure keeps invoices on purpose: a valid invoice has to carry the buyer's name and address and has to be
kept for years, so the right to erasure yields to the retention obligation. Those rows are unlinked from the
owner and removed by billing:prune once the window closes. See
Data protection and
Retention and erasure.
DATEV export
| Key | Env | Default | What it does |
|---|---|---|---|
billing.datev.consultant | BILLING_DATEV_CONSULTANT | null | The consultant number in the EXTF header. |
billing.datev.client | BILLING_DATEV_CLIENT | null | The client number in the EXTF header. |
billing.datev.account_length | BILLING_DATEV_ACCOUNT_LENGTH | 4 | The account-number length your chart uses. |
billing.datev.business_year_start | BILLING_DATEV_BUSINESS_YEAR_START | 01-01 | The first day of your business year, as MM-DD. The batch header names the start of the business year the batch falls in, so a business year that begins on 1 July is 07-01. Anything that is not a day of the year stops the export with the key named. |
billing.datev.revenue_account | BILLING_DATEV_REVENUE_ACCOUNT | null | The revenue account every invoice books against when no chart is selected. Left empty, the export still produces a structurally valid file with blank account fields. |
billing.datev.customer_account | BILLING_DATEV_CUSTOMER_ACCOUNT | null | The receivables account every invoice books to when no chart is selected. |
billing.datev.chart | BILLING_DATEV_CHART | null | skr03, skr04 or null. Null uses the two accounts above and the export is byte-identical. Selecting a chart changes only the values resolved per transaction, never the file's structure or field order. |
billing.tax_oss.correction_window_years | BILLING_TAX_OSS_CORRECTION_WINDOW_YEARS | 3 | How long a filed period may still be corrected, measured from its due date — a month later than the period end, and measuring from the wrong one lets through a correction that is already out of time. Past the window the export refuses rather than dropping the line. |
billing.reporting.goods_de_minimis.max_sales | BILLING_REPORTING_MAX_GOODS_SALES | 30 | Where a small-scale exemption for sales of goods stops on the count. Both edges must hold at once — one alone exempts nobody. |
billing.reporting.goods_de_minimis.sales_operator | BILLING_REPORTING_SALES_OPERATOR | < | How the count is compared. |
billing.reporting.goods_de_minimis.max_compensation_minor | BILLING_REPORTING_MAX_GOODS_COMPENSATION_MINOR | 200000 | Where it stops on the amount. |
billing.reporting.goods_de_minimis.compensation_operator | BILLING_REPORTING_COMPENSATION_OPERATOR | < | How the amount is compared — strict by default, as § 4 Abs. 5 PStTG words it: less than the figure. The directive reads "did not exceed", which would also exempt the seller sitting exactly on the figure; set <= only if your statute follows that wording. |
billing.e_invoice.always | BILLING_E_INVOICE_ALWAYS | null | Whether documents are issued electronically. Null asks the active jurisdiction profile — unset is not the same as false, because reading a consumer's silence as "no" would mean an operator whose jurisdiction requires it silently does not comply. |
billing.invoices.pdf_disk | BILLING_INVOICE_PDF_DISK | null | The disk your billing_invoices.pdf_path values address. Name it and the download route serves the PDF you kept instead of rendering a new one, under every driver — which matters because a re-render years later resembles the document your customer holds without being it. Under a local-engine driver, the invoice list offers the download of a kept file even without a PDF renderer. Left null, no disk is ever touched and the route behaves exactly as before. A recorded path whose file is gone falls back to a fresh render where a PDF renderer is bound, so an owner is not locked out of their own invoice, and it is logged as an error: a lost archive file is an incident, and the quiet version of it is the expensive one. |
billing.invoices.number_prefix | BILLING_INVOICE_NUMBER_PREFIX | INV | The prefix on an invoice number the package mints itself: PREFIX-YYYY-0000001. A local engine's invoices carry it, and so does every receipt from the counter. The running part restarts each year, which is what keeps a number readable. A provider-driven driver's own invoices never reach this: they copy the number the provider already issued. Gaps in the sequence are harmless; a number issued twice is not, so the sequence locks rather than counting rows. |
billing.card_present.small_amount_threshold_minor | BILLING_CARD_PRESENT_SMALL_AMOUNT_MINOR | 25000 | The gross, in minor units, up to and including which a sale paid by card at the counter gets a simplified invoice (€250.00, the German § 33 UStDV figure). Above it the buyer is still anonymous, so the document is a payment record. A seller whose counter stands in a country with another limit sets that country's figure. |
billing.tax_us.enabled | BILLING_TAX_US_ENABLED | false | Whether anything acts on the tax declarations sellers give (withholding, reporting). The declarations are collected either way — asked for at onboarding or chased a year later from sellers who have moved, under a filing deadline, which ends in withholding money from people who did nothing wrong. The identifying number itself is never stored. |
billing.retention.place_evidence_days | BILLING_RETENTION_PLACE_EVIDENCE_DAYS | 3650 | How long the evidence for a sale's country is kept, counted from the end of the year of the sale. Deliberately longer than the document window and its own key — merging them would prune the evidence before the return it justifies stops being examinable. billing:prune keeps an erased person's evidence for this window, and never for less than erased_financial_days. |
billing.tax_oss.binding_years | BILLING_TAX_OSS_BINDING_YEARS | 2 | How many years a declaration that gave up the distance-sale threshold binds for. Checked at boot: withdrawing it early fails loudly rather than falling back to the seller's own rate on supplies that owe the destination's. |
billing.tax_oss.export_disk | BILLING_TAX_OSS_EXPORT_DISK | null | Filesystem disk a produced tax-return file, and the recapitulative statement beside it, is copied to. Null writes nowhere — what was produced is recorded either way. The package files nothing with any authority and holds no portal credentials. |
billing.tax_oss.export_path | BILLING_TAX_OSS_EXPORT_PATH | tax-returns | Directory on that disk, for both declarations. |
billing.tax_us.activation_share_bps | BILLING_TAX_US_ACTIVATION_SHARE_BPS | 5000 | How close to a region's limit counts as approaching it, in basis points of that limit. Waiting for a limit to be crossed is waiting too long — registration takes weeks while the obligation starts at the crossing. |
billing.tax_rate_probe.enabled | BILLING_RATE_PROBE | false | Whether billing:rates:probe may reach the public source that publishes VAT rates. Off by default: a package should not contact a public service because it happened to be installed. Turn it on for a nightly job, never for the push gate — a network-dependent check there goes red on the first DNS hiccup and gets disabled, which is how an arm quietly disappears. |
billing.tax_rate_probe.proposal_path | BILLING_RATE_PROPOSAL_PATH | null | Where billing:tax-rates:check writes its proposal. Null means beside the shipped snapshot, so the two share a dating convention and can be diffed side by side. Point it somewhere writable when the installed package is read-only, or when the next release would take the file with it — a proposal exists to be reviewed by a person, which can be days later. The snapshot itself is never written to either way: this package proposes rate changes and does not apply them. |
billing.tax_exchange_rates.enabled | BILLING_EXCHANGE_RATES | false | Whether exchange rates are read from the local billing_exchange_rates table. Off by default, and off means a refusal rather than a silence: every conversion is answered by saying the package ships none and what to do about it. It ships none because which rate is correct is jurisdiction knowledge and the rules contradict each other — the ministry's monthly average against the central bank's rate at the tax point against its rate at period end, on the same turnover. Turn it on and the numbers on your documents are the ones you imported, from a publisher you can name. A single-currency install never converts and never reaches any of this. |
billing.marketplace.tax_status_hold.enforce_from | BILLING_MARKETPLACE_HOLD_ENFORCE_FROM | null | The day the creator tax-status hold starts refusing sales for merchants whose taxation nobody has established. Null until you choose one, and that is not the two switches beside it being ignored — they say what is held, this says from when. It needs a date because its default refuses everybody: a merchant nobody has declared for is Unclarified, which is exactly the standing that blocks, so switching it on with today's date stops every creator who has not yet declared. billing:marketplace:preflight reports an unset date as outstanding. A value that is not a date is refused outright rather than guessed at, and a jurisdiction profile that requires the hold overrides this. |
billing.marketplace.tax_status_hold.warn_days_before | BILLING_MARKETPLACE_HOLD_WARN_DAYS_BEFORE | 30 | How many days before the date above the merchants who have not declared are told it is coming. The line beside enforce_from says to tell them; this says when. Too early is forgotten by the time it matters, and too late is not a warning — producing a declaration takes longer than a checkout takes to fail. Nobody is warned while enforce_from is unset: with no deadline there is nothing to warn about, and inventing one to have something to say is worse than silence. Sent once per deadline, and again if you move the date, because a merchant told about March has not been told about June. |
billing.reporting.filing_notice_days | BILLING_FILING_NOTICE_DAYS | 14 | How many days before a filing obligation falls due it is announced. Long enough to assemble the figures, short enough that the notice is still about something imminent — a reminder six weeks out is filed away and not seen again. Each obligation is announced once for its date, and the two that share the end-of-January deadline are announced separately: different law, different data, and whoever handles the one they thought of must not be able to consider the day dealt with. |
billing.tax_exchange_rates.currencies | — | [] | Which currencies billing:exchange-rates:import fetches, as the bank names them. Empty by default, so the switch above alone still contacts nobody: holding rates locally and knowing which currencies you settle in are two decisions, and a package that inferred the second would dial out for currencies nobody sells in. Rates are stored in the direction the bank publishes them — euro to each of these — and never turned around, so list the currencies you receive money in. |
billing.tax_exchange_rates.max_age_days | — | 3 | How many days old the newest imported rate may be before billing:doctor calls the series out. Three, deliberately well under the fourteen at which the lookup gives up and a document cannot be issued: three days is two missed daily imports plus a weekend, which is a warning, while fourteen is the incident already happening. The forward window stays the ceiling — a value above it would let the diagnostic report green while the money path is refusing documents, so the lower of the two applies. The check is silent when the store is switched off or no currency is listed. |
billing.tax_exchange_rates.lookback_days | BILLING_EXCHANGE_RATES_LOOKBACK_DAYS | 10 | How far back a scheduled import re-fetches. More than a day on purpose: a publisher revises, a run is missed, a machine sleeps through a night. Importing is idempotent, so an overlap costs a few rows rewritten with the same figures — while a one-day window turns any missed run into a permanent hole in the series, and the reader answers a hole with the next publication day's rate. |
billing.tax_point_on_receipt | BILLING_TAX_POINT_ON_RECEIPT | false | Whether a prepayment is taxed when the money arrives rather than as the service is rendered. On a prepaid year the two are eleven months apart. Off by default — a silent change of tax period is the last thing an upgrade should do. |
billing.datev.person_accounts.mode | BILLING_DATEV_PERSON_ACCOUNTS | collective | How merchant payables appear in the books. collective books every merchant against the one liabilities account and keeps the per-merchant detail in this package's sub-ledger — workable at any number of merchants. individual gives each merchant their own account, for an accountant who expects open items per creditor. The booking logic is identical; only the account the payable resolves to changes. Switching an install that has already booked is a documented migration, not a flag flip. |
billing.datev.person_accounts.range_start | BILLING_DATEV_CREDITOR_RANGE_START | 70000 | Where individual creditor numbers begin. A number is allocated on first use and kept — one that moved would leave earlier bookings pointing at an account that now means somebody else. |
billing.datev.accounts | — | an SKR03 and an SKR04 map | The per-transaction account map for each chart. Each entry is account plus automatic; an automatic account derives its VAT from the posting itself, so a tax key is never set alongside it. A reverse-charge entry may add reverse_charge_transaction_key, the three-digit key that says under which circumstance the reverse charge is assessed — confirm it with your accountant the same as the account number, and note that zero is not a permitted value. Configure none and the export keeps the fields it always wrote. The numbers are German-accountant defaults, not values the package invents — override them for a different frame without a code change. |
These are specific to your chart of accounts and your tax advisor's setup. Confirm them before importing. See Accounting and DATEV.
Multi-merchant keys
The package ships the single-merchant path. These keys exist because two guarantees have to be enforceable before a routed sale could ever exist, and both are enforced today: a posture cannot be resolved outside the list you opted into, and the platform cannot start holding other people's money by flipping a flag.
| Key | Env | Default | What it does |
|---|---|---|---|
billing.content_ownership.enabled | BILLING_CONTENT_OWNERSHIP_ENABLED | false | Turns on the content-ownership register — what a buyer owns, as opposed to what their plan lets them do. The licensing side (config/license.php, reached through the License contract) answers "what may this owner do right now", and its answer changes the moment their tier does; this answers "what did this person buy, and is it still theirs", which outlives the plan, the creator's account and the work's own publication. Off, the read seam resolves to a reader that answers no to everything -- an answer rather than a resolution error, so "off" and "miswired" stay distinguishable at the call site. The three effects that would otherwise write or withdraw a grant (a completed purchase, a refund, a chargeback) read this key first and return before touching anything, so off is fail-closed on both sides rather than only on the read. |
billing.content_ownership.default_update_policy | BILLING_DEFAULT_UPDATE_POLICY | latest | What a sale promises about later versions when neither the work nor the merchant says otherwise: latest, latest_with_revisions, windowed, or frozen. This is the enrichment axis -- new editions, added material -- and it is the creator's to choose. It is not the conformity axis: a defect fix or a security patch is owed regardless of what stands here, which is why no value switches those off. A misspelt value is refused at first use rather than defaulted, because falling back to latest would promise free updates forever on every sale the install ever makes. windowed needs a length, and the length comes from your catalog, not from here: implement SuppliesUpdateWindows beside your UpdatePolicyCatalog and answer windowForContent() / windowForMerchant() with a CarbonInterval. "Updates for twelve months" is a term of sale a creator sets per work, so a configured global would put a term on every sale nobody agreed to. Answer nothing and a windowed grant is written with no window, which the resolver reads fail-closed as the moment of purchase — the same result frozen gives. |
billing.content_ownership.bundle_additive_default | BILLING_BUNDLE_ADDITIVE_DEFAULT | false | Whether a work added to a bundle later reaches people who bought the bundle earlier. Off, because a bundle is normally what it was on the day it was bought -- and off costs nothing to enforce, since bundle grants are materialized at purchase and a work added next month simply has no row for an earlier buyer. On, a repeat grantBundle() call for the same buyer tops up what has since been added. |
billing.content_ownership.revoke_on_refund | BILLING_REVOKE_ON_REFUND | true | Whether a refund also ends access to the work. Genuinely switchable, because both answers are somebody's deliberate policy: leaving access in place after a goodwill refund is common and often the point -- the work has already been read, so taking it back costs nothing to skip and turns a recovered customer into an angry one. Ending it is right where the refund was a return rather than a gesture. |
billing.content_ownership.revoke_on_chargeback | BILLING_REVOKE_ON_CHARGEBACK | true | The same question for a lost dispute, and a different situation: a chargeback is involuntary, decided by somebody else, and the money is already gone. There is no version of it where the platform chose to give the work away, which is why the shipped answer is to end access. This switch also decides where a driver reports the lost dispute as a refund of the purchase, and the revocation then records chargeback as its reason. |
billing.content_ownership.restore_on_chargeback_reversal | BILLING_RESTORE_ON_CHARGEBACK_REVERSAL | true | Whether access a chargeback ended comes back when the bank takes the chargeback back. On, because the customer has paid after all. Only a grant the chargeback revoked returns: one that ended for another reason stays revoked, and the revocation stays on the row beside reinstated_at. Only Mollie reports a reversal, from billing:money-back:reconcile |
billing.consumer_rights.conformity_update_period_days | BILLING_CONFORMITY_UPDATE_PERIOD_DAYS | null | How long a seller owes conformity updates on a sale -- defect fixes, security fixes, staying compatible. A different axis from content_ownership.default_update_policy, which governs what the creator sells and may withhold; this governs what is owed afterwards regardless, so a frozen sale still receives them. Ships empty, and empty is not an oversight: the obligation runs for as long as a buyer may reasonably expect, that is a judgment about a kind of product, and no statute states a number -- a package shipping one would be inventing a legal answer and hiding it in a library. Empty means no end has been established and updates keep flowing. |
billing.consumer_rights.allow_conformity_waiver | BILLING_ALLOW_CONFORMITY_WAIVER | false | Whether a buyer can validly agree to give that obligation up at all in your jurisdiction. It switches nothing off: there is deliberately no install-wide off switch for conformity updates, because that is exactly the blanket arrangement the law refuses to recognize. It only makes a waiver possible one grant at a time, and recording one still needs a reference to an actual agreement made separately and before the contract -- a flag in a file cannot produce a declaration. Off, because whether security fixes can be waived at all is genuinely disputed; turning it on is your decision on your own advice. |
billing.marketplace.enabled | BILLING_MARKETPLACE_ENABLED | false | Off, the custody guard below has nothing to check and single-merchant behavior is byte-identical. |
billing.marketplace.custody.platform_held | BILLING_MARKETPLACE_PLATFORM_HELD | false | Whether the platform itself would hold other people's funds. Holding funds is a regulated activity in most jurisdictions, so this is refused at boot unless the host binds a PaymentServiceLicenseAttestation. A flag alone can never make an unaware consumer an unlicensed money holder. There is deliberately no interest or yield option. |
billing.marketplace.seller_of_record.default_posture | BILLING_MARKETPLACE_POSTURE | platform_deemed_supplier | Who the seller is to the buyer. A liability and VAT decision the package enforces but never makes for you. It does not decide where a merchant-defined tier's price is minted: that is the platform account under every posture, because the checkout runs there for both charge types. |
billing.marketplace.seller_of_record.allowed_postures | — | ['platform_deemed_supplier'] | The postures you have deliberately opted into. Resolving one outside this list is refused. |
billing.marketplace.seller_of_record.supplies_are_electronic | BILLING_MARKETPLACE_SUPPLIES_ELECTRONIC | true | Default classification of what is sold. An electronically supplied service falls under the deemed-supplier presumption; physical goods do not. |
billing.marketplace.seller_of_record.art9a_rebuttal_asserted | BILLING_MARKETPLACE_ART9A_REBUTTAL | false | The rebuttal assertion. It and the three keys below must all be true before a seller-of-record posture is allowed for an electronic supply. |
billing.marketplace.seller_of_record.no_agb_control | — | false | Assert that you do not set the terms of the supply. |
billing.marketplace.seller_of_record.no_billing_authorization | — | false | Assert that you do not authorize the charge to the buyer. |
billing.marketplace.seller_of_record.no_supply_authorization | — | false | Assert that you do not authorize the delivery of the supply. |
billing.marketplace.deemed_supply_of_goods | — | the keys below | Goods whose sale makes the platform their supplier: an imported consignment worth up to 150 euros, whoever sells it, and goods in the Union sold by a seller established outside it to somebody who is not a business. A routed sale of goods is classified from the consignment its lane passes. Under a tax profile that documents such a sale it goes ahead as the platform's own supply; under any other it is refused. See the marketplace guide. |
billing.marketplace.deemed_supply_of_goods.ioss_number | BILLING_MARKETPLACE_IOSS_NUMBER | null | The number the platform is registered under in the import scheme, IM followed by ten digits. An imported consignment the platform supplies is refused without a valid one. |
billing.marketplace.deemed_supply_of_goods.union_scheme | BILLING_MARKETPLACE_DEEMED_SUPPLY_UNION_SCHEME | false | Your statement that the platform declares its supplies of goods already in the Union in the Union scheme. The receipt the package issues for such a supply is enough only there, so a deemed supply under Art. 14a(2) is refused until it is true. |
billing.marketplace.seller_activity | — | the two keys below | When a seller is active enough to be asked to declare their standing. |
billing.marketplace.seller_activity.sales_threshold | BILLING_MARKETPLACE_SALES_THRESHOLD | 30 | Sales in the period that count as trading. |
billing.marketplace.seller_activity.proceeds_threshold_minor | BILLING_MARKETPLACE_PROCEEDS_THRESHOLD_MINOR | 200000 | Proceeds in the period that count as trading, in minor units. Two thresholds read these numbers and they are not the same rule: the declaration is asked for once either measure is reached, the reporting exemption holds only while both stay below. At exactly the figure they agree, because the German statute exempts only less than it; they stay separate rules all the same, because the exemption's boundary is set by law and the declaration's by you. |
billing.marketplace.seller_activity.sales_comparison | BILLING_MARKETPLACE_SALES_COMPARISON | >= | How the sales count is compared with its threshold: >= (reaching it is enough) or >. |
billing.marketplace.seller_activity.proceeds_comparison | BILLING_MARKETPLACE_PROCEEDS_COMPARISON | >= | The same for the proceeds. |
billing.marketplace.seller_activity.window | BILLING_MARKETPLACE_ACTIVITY_WINDOW | calendar_year | What the two figures are counted over: calendar_year, or rolling_year for the 365 days up to now. |
billing.marketplace.seller_data_escalation | — | the keys below | What happens when a seller does not supply the data a reporting duty needs. |
billing.marketplace.seller_data_escalation.first_reminder_after_days | BILLING_MARKETPLACE_FIRST_REMINDER_DAYS | 7 | Days incomplete before the first reminder. |
billing.marketplace.seller_data_escalation.second_reminder_after_days | BILLING_MARKETPLACE_SECOND_REMINDER_DAYS | 30 | Days before the second. |
billing.marketplace.seller_data_escalation.measure_after_days | BILLING_MARKETPLACE_MEASURE_AFTER_DAYS | 60 | Days before a measure applies — and only where the missing data is legally required of that seller. |
billing.marketplace.seller_data_escalation.measure | BILLING_MARKETPLACE_DATA_MEASURE | withhold_payout | suspend_sales or withhold_payout. Two very different impositions, and neither is obviously the gentler one: stopping somebody selling ends their income, holding their money leaves them selling and unpaid. |
billing.marketplace.seller_data_escalation.measure_precautionary_gaps | BILLING_MARKETPLACE_MEASURE_PRECAUTIONARY | false | Whether a measure also applies where only precautionary data is missing. Off, because extending a sanction to data no law requires is a contract question between a platform and its sellers. |
billing.marketplace.seller_data_escalation.withhold_up_to_days | BILLING_MARKETPLACE_WITHHOLD_UP_TO_DAYS | 90 | How long a withholding may last. |
billing.marketplace.seller_data_escalation.payout_deadline_days | BILLING_MARKETPLACE_PAYOUT_DEADLINE_DAYS | 90 | The money rail's own deadline, read here and defined by the payout schedule. A withholding is capped by it: "held until they cooperate" is open-ended and the rail is not, and a hold that outran it would not be stricter — it would be a payment nobody can complete. |
billing.marketplace.seller_data_escalation.on_withholding_exhausted | BILLING_MARKETPLACE_ON_WITHHOLDING_EXHAUSTED | suspend_sales | What follows when a withholding has run as long as it may without the data arriving. The money moves either way, because the rail's limit is not negotiable: suspend_sales turns the withholding into a suspension, release ends it with the money. |
billing.marketplace.seller_record | — | the key below | What a platform asks a seller for at onboarding. |
billing.marketplace.tax_standing_route | BILLING_MARKETPLACE_TAX_STANDING_ROUTE | null | The route of your screen where a creator declares or renews their tax standing. The package has none; named here, the reminder that an attestation is due links to it, and without it the reminder carries no button. |
billing.marketplace.seller_record_route | BILLING_MARKETPLACE_SELLER_RECORD_ROUTE | null | The route of your screen where a seller completes their record. The record lives in your application, so the screen does too; named here, a reminder about missing seller details links to it, and without it the reminder carries no button. |
billing.marketplace.seller_record.collect_precautionary | BILLING_MARKETPLACE_COLLECT_PRECAUTIONARY | true | Collect the fields a reporting duty adds from every seller, not only those the duty currently covers. A seller's classification changes the day they take on different work, and a platform that only asked the sellers it already knew about then has to chase the rest — after the year closed, under a deadline, from people who have gone quiet. Switch it off and those fields are asked for only once a duty applies; the fields needed to settle at all are never affected either way. |
billing.marketplace.tax_status_hold | — | the two keys below | What happens while a merchant's tax standing is unestablished. |
billing.marketplace.tax_status_hold.blocks_payouts | BILLING_MARKETPLACE_HOLD_BLOCKS_PAYOUTS | true | Hold the merchant's own earnings until their standing is recorded. Inert under a jurisdiction profile that requires the hold: turning it off there would amount to choosing a default standing for people whose standing nobody knows. |
billing.marketplace.tax_status_hold.blocks_sales | BILLING_MARKETPLACE_HOLD_BLOCKS_SALES | true | Stop selling on their behalf too. Holding only the payout is the more dangerous half-fix: buyers' money keeps arriving and each transaction creates an obligation to settle that cannot be settled — a growing backlog instead of a stop. Equally inert under such a profile. |
billing.marketplace.regime | — | the two keys below | Which shape a routed sale has. The platform reselling in its own name, or the platform arranging somebody else's sale — they produce different documents, different turnover, and different parties on a receipt. |
billing.marketplace.regime.default | BILLING_MARKETPLACE_REGIME | commission_chain | The shape a sale has when what is sold does not decide for itself. commission_chain or intermediation. An unreadable or unknown value is refused rather than defaulted: silently choosing here would pick which documents every sale produces, on the strength of a typo. |
billing.marketplace.regime.allowed | — | ['commission_chain'] | The regimes you have deliberately opted into. Resolving one outside this list is refused — a platform that has not said it arranges other people's sales must not begin to because a product was classified in a way nobody looked at. |
billing.marketplace.numbering | — | the key below | Prefixes for the document series the platform numbers itself. |
billing.marketplace.numbering.series | — | the eight keys below | One visible prefix per document role. The number format is PREFIX-YYYY-####### — the prefix, the four-digit year, and a seven-digit running number that restarts at 1 each year within the series. A role with no configured prefix is refused at allocation rather than numbered with a blank, because a malformed number issued is itself a numbered event that would then need a correction. These are the German defaults; a consumer elsewhere maps the same roles to their own letters. |
billing.marketplace.numbering.series.buyer_receipt | — | F | Prefix for the buyer's receipt. |
billing.marketplace.numbering.series.self_billed_invoice | — | G | Prefix for a self-billed invoice to a merchant. |
billing.marketplace.numbering.series.settlement_note | — | A | Prefix for a settlement note to a private party. |
billing.marketplace.numbering.series.commission_invoice | — | P | Prefix for a commission invoice — drawn only in an intermediation sale, never in a commission chain. |
billing.marketplace.numbering.series.buyer_receipt_correction | — | KF | Prefix for a correction to a buyer receipt. |
billing.marketplace.numbering.series.self_billed_invoice_correction | — | KG | Prefix for a correction to a self-billed invoice. |
billing.marketplace.numbering.series.settlement_note_correction | — | KA | Prefix for a correction to a settlement note. |
billing.marketplace.numbering.series.commission_invoice_correction | — | KP | Prefix for a correction to a commission invoice. |
billing.marketplace.self_billing | — | the keys below | Whether the platform may settle a creator by self-billing, and on what precondition. |
billing.marketplace.self_billing.enabled | BILLING_MARKETPLACE_SELF_BILLING_ENABLED | true | Whether the platform settles creators by self-billing at all. On by default. A consumer that does not self-bill turns this off and stays in the fallback lane — the creator submits their own invoice. It is a backstop, not the routing decision: check it before settling, and the engine refuses loudly if it is reached while off rather than issue a document a disabled platform never meant to. |
billing.marketplace.receipts | — | the key below | What the buyer's receipt collects, and when. |
billing.marketplace.receipts.small_amount_threshold_minor | BILLING_MARKETPLACE_RECEIPT_SMALL_AMOUNT_MINOR | 25000 | The gross, in minor units, up to and including which a domestic purchase gets the simplified receipt (€250.00, the German § 33 UStDV figure). A larger or cross-border purchase gets a plain payment record; only a buyer who asks for a full invoice has their name and address collected. A consumer sale carries no invoicing duty, so the least document is issued and the least data collected. |
billing.marketplace.self_billing.require_agreement | BILLING_MARKETPLACE_SELF_BILLING_AGREEMENT | true | Whether a self-billed document requires a prior agreement with the creator. On by default; it opts out only explicitly. A self-billed document is an invoice only if both sides agreed to the arrangement before it — one issued without that agreement is not an invoice and cannot be repaired. A jurisdiction that does not demand the agreement turns this off, but a missing or non-boolean value keeps the requirement, because the fail-safe is to require it. The clause text and the onboarding screen belong to you; the package ships only the record and the guard. |
billing.marketplace.fallback.tolerance_minor | BILLING_MARKETPLACE_FALLBACK_TOLERANCE_MINOR | 0 | How far, in minor units, a fallback-lane invoice's net or tax may deviate from what the creator earned that period before it is a review finding. The default is exact: the platform pays out what the creator earned, not what they wrote, and a mismatch holds the payout. A creator submits their own invoice through the fallback lane after an objection or a terminated self-billing agreement; the reconciliation is what keeps a €300.00 invoice against €270.00 earned from being paid. |
billing.marketplace.fee.rounding | BILLING_MARKETPLACE_FEE_ROUNDING | platform_first | Which side of an uneven percentage split keeps the leftover minor unit. platform_first gives it to the fee; creator_first gives it to the net, the only order that hits an exact target payout. At volume that assignment is real money, so it is a documented contract choice rather than an accident of rounding. |
billing.marketplace.fee.refund_policy | BILLING_MARKETPLACE_FEE_REFUND_POLICY | refund | What happens to the platform's own commission when a sale is unwound. refund gives back the part of it that what remains of the sale no longer carries, recomputed on the remainder because a fixed part is not halved by a half refund — the only value under which a refund nets to zero across all three parties. On a destination charge the package gives that amount back itself, after the refund, rather than letting Stripe share out the fee it first withheld. retain keeps it, which leaves the merchant short by that amount. retain is refused under the commission_chain regime, at preflight rather than at the first refund: there the platform buys and resells, so unwinding the sale unwinds both supplies and no commission document remains for a retained fee to sit on. |
billing.subscribe_return_url | BILLING_SUBSCRIBE_RETURN_URL | null | Where a provider without a synchronous setup call returns the customer after the first payment that establishes their mandate. Falls back to billing.checkout.success_url. Set neither and starting a subscription is refused rather than redirected to nowhere — the alternative is a customer who completes a real payment and lands on an error page holding a mandate nothing told them about. |
billing.mandate_verification_minor | BILLING_MANDATE_VERIFICATION_MINOR | 1 | What that first payment charges, in minor units of the plan's currency. It appears on the customer's statement, which is why it is yours to choose. The default is the smallest unit the currency has rather than the plan price, on purpose: this payment exists to create a mandate, and the first cycle is billed by the engine on its own schedule — collecting it here as well would charge the customer twice for one period. |
billing.collection_timing | BILLING_COLLECTION_TIMING | in_advance | When a driver the package bills itself collects each period of a new subscription. in_advance charges a period when it begins, the first one right after the mandate exists or when the trial ends, and squares a swap or a seat change inside a paid period at once: a dearer change is charged for the time left, a cheaper one leaves credit. in_arrears charges a period when it ends and prices a swap or a seat change into that bill. Each subscription keeps the timing it started with, so a change here reaches new subscriptions only, and one started before the setting existed is collected in arrears. Any other value refuses to boot. |
billing.consumer_rights | — | the keys below | The buyer's statutory withdrawal rights. This is consumer law, not tax law, and deliberately its own profile — a single seller needs it too, and an operator may run one country's VAT and another's consumer regime. |
billing.consumer_rights.profile | BILLING_CONSUMER_RIGHTS_PROFILE | null | Which consumer-rights reading is in force. Unset, empty and false all mean off: no extra checkout step, no changed receipt, byte-identical. de selects the German reading the package ships. A name of your own needs readings of your own bound for ConsumerWithdrawalPolicy and ConformityUpdatePolicy; any other value, off included, is refused with an InvalidBillingConfig at the first question the regime is asked, and billing:doctor reports it. Set it and the gate is fail-closed for every work that carries a classified archetype — such a work is not provided until the buyer's double consent is recorded, because providing it first forfeits their refund right on their behalf. The profile is one of two conditions, not the only one: at provision, a work nobody classified has no withdrawal type to gate on. That is caught one step earlier instead — PurchaseDeclarations::assertMayCheckout() refuses to start a checkout for an unclassified work while a profile is active, so the purchase never happens rather than the work being handed over unconsented. The shipped account hub calls it; a checkout of your own has to call it too, and until it does, the provision path still hands such a work over. A subscription checkout of your own calls its twin, assertMaySubscribe(), which reads a tier without an archetype as a subscription. Classify your works (archetype on the add-on, or your own SuppliesProductArchetypes), and run billing:doctor — it names any work that this profile does not actually cover. |
billing.consumer_rights.declaration_timezone | BILLING_DECLARATION_TIMEZONE | Europe/Berlin | The time zone a declaration through the cancellation button states the moment it arrived in, on the confirmation mail and on every saved copy. That moment decides whether the declaration was in time, and a receipt time that shifts with whoever reads it is not one time, so the zone is named beside it. Set the zone of the business that receives the declaration; a value that names no zone states the moment in UTC. |
billing.consumer_rights.declaration_receipt_route | BILLING_DECLARATION_RECEIPT_ROUTE | null | A route of your own that shows the receipt of a declaration, given the declaration's token as its declaration parameter. When it is set and registered, the confirmation mail links to it. The package draws no receipt page of its own. |
billing.ai | — | the keys below | What model calls cost, and the daily caps on it. See the AI costs page. |
billing.ai.currency | — | USD | The currency the rates and the caps are written in, where a rate does not name its own. |
billing.ai.rates | — | [] | The price list, keyed by model id and read by array access, because a model id carries dots. Each model holds one entry or a list of entries with a valid_from date, priced per million tokens as decimal strings: input and output, and cache_read, cache_write and reasoning where the provider prices them apart. A call is priced at the entry of its own day. |
billing.ai.daily_caps | — | [] | What one provider's calls may cost per UTC day, keyed by provider name, as decimal strings in billing.ai.currency. A provider without a cap is counted and never refused. |
billing.ai.owner_daily_cap | — | null | What one owner's calls may cost per UTC day, as a decimal string in billing.ai.currency. Null counts and never refuses. |
billing.tax_evidence | — | the key below | How the buyer's country is established from the signals available while the sale happens. |
billing.tax_evidence.required_signals | BILLING_TAX_EVIDENCE_SIGNALS | 2 | How many sources must name a country before a sale can rest on them. The legal answer depends on turnover — one piece of evidence below a threshold, two non-contradicting ones above it — so the package ships the stricter setting: too much evidence costs a checkout question, too little costs a defensible position. A value that cannot be read is refused rather than defaulted, because silently becoming 1 would relax the standard. |
billing.tax_evidence.collect_subdivision | BILLING_TAX_EVIDENCE_SUBDIVISION | true | Whether a sale's subdivision — a US state — is recorded beside its country. On, because it cannot be turned on retroactively: a US sales-tax nexus is measured per state over a rolling window, the evidence is written once at the sale, and the raw IP behind it is deliberately discarded — so a state not captured then is gone, and a counter built afterwards can only fill an unknown bucket while looking as though it worked. It records nothing you did not already supply: this package has no input finer than the country and does not go looking for one, so a sale whose signals carry no subdivision writes none. And it is narrow by construction — only for the countries in subdivision_countries, only from the sources that named that country, only when they agree, and only the ISO 3166-2 suffix (CA, never US-CA), never a postcode, a city or a coordinate. Off, the rest of the evidence is untouched and a state counter runs honestly on unknown rather than quietly on a guess. |
billing.tax_evidence.subdivision_countries | — | ['US'] | Which countries' subdivisions are worth recording at all. A list rather than „everywhere one exists", because almost every country has subdivisions and almost none of them decide anything this package is asked about. The US is here because its sales tax is administered per state and the registration duty follows a per-state threshold; adding a country should meet the same test — a question the subdivision actually answers. |
billing.tax_markets | — | null | The countries you are registered in and may sell into, as ISO code → open | planned | blocked. Codes may be written in either case; two spellings of one code are refused. Absent means no gate at all — a check defaulting to closed would stop every existing install at its next sale, which is an outage rather than a guard. Once configured it is fail-closed: anything not explicitly open is refused, including a country the evidence could not resolve. Refused BEFORE the payment where the country is known, because a sale into a country with no registration cannot be repaired by any later document: a checkout handed buyerCountry checks it before the session opens. Under Stripe the country the provider taxed a sale in is checked again once it is reported, and a sale into a market that is not open is undone, the subscription ended, the payment refunded and SaleIntoClosedMarketReversed dispatched. A market opened here that the local rates cannot price refuses the boot. Monaco is priced, placed and reported as France, as VAT law treats it, so MC may be opened like any member state. A sibling of billing.tax, never a child — nesting under that scalar would turn it into an array and silently produce 0% tax on every invoice. |
billing.marketplace.vouchers.enabled | BILLING_VOUCHERS_ENABLED | false | Whether vouchers exist at all. Off by default: a balance customers pay into is a supervised question the moment it can be recharged, cashed out or handed on. This one can do none of those — there is no method for any of them — but it is still something to switch on knowingly. |
billing.marketplace.vouchers.instrument_type | BILLING_VOUCHER_INSTRUMENT_TYPE | multi_purpose | When the tax on a voucher falls. Frozen on each voucher at issue — a supply already made cannot be re-decided by a later change here. multi_purpose where you sell into many countries at many rates (nothing taxed at issue); single_purpose where one country and one rate make the supply determined already. |
billing.marketplace.vouchers.expire_after_days | BILLING_VOUCHER_EXPIRE_AFTER_DAYS | 1095 | How long a voucher stays spendable. Whether a given period is enforceable against a consumer is a legal question this package does not answer — it makes the period configurable and records it. |
billing.marketplace.vouchers.volume_window_months | BILLING_VOUCHER_VOLUME_WINDOW_MONTHS | 12 | The rolling window the volume counter measures over. |
billing.marketplace.vouchers.volume_threshold_minor | BILLING_VOUCHER_VOLUME_THRESHOLD_MINOR | 100000000 | The figure past which a filing is expected where you operate. The counter reports it; filing anything is a decision a person makes. The figure counts two sources, so do not read "we issue no vouchers" as "the counter is moot": vouchers issued in the window, plus paid credit top-ups — an add-on its catalog classifies as money credit credits the balance at face value, which is the same instrument in another table, and each purchase counts by what it was when it was made. Refunds come off both. Currencies are kept apart rather than converted, because a rate this package picked would put a figure nobody published under a supervisory number. |
billing.marketplace.vouchers.volume_warn_at_percent | BILLING_VOUCHER_VOLUME_WARN_AT_PERCENT | 80 | Where the early warning sits. A threshold you learn about on the day you cross it leaves no time to act. |
billing.marketplace.charge_type | BILLING_MARKETPLACE_CHARGE_TYPE | separate_transfer | Which money flow you use with your provider. It has to agree with your seller-of-record posture, and the pairing is checked before a routed payment is assembled — not after one has been sent and only the transfer is left to fail. |
billing.marketplace.buyer_protection | — | the five keys below | A payout that waits until the buyer confirms, or until their silence has lasted long enough to count as consent. The money is never held by your application — it stays with the payment provider and a release is an instruction to it. |
billing.marketplace.buyer_protection.enabled | BILLING_BUYER_PROTECTION | false | Whether the merchant's share waits instead of moving the moment the payment succeeds. Off by default, and that default is the whole safety of the switch: with it off the payment path is byte-identical to what it always was, so no installation changes behavior on an upgrade. Turning it on decides when a seller is paid, which is not a call a package may make for an operator. It applies to the separate_transfer lane only — on a destination charge the provider moves the money as the payment settles, and there is no moment in between to hold. |
billing.marketplace.buyer_protection.account_type | BILLING_BUYER_PROTECTION_ACCOUNT_TYPE | express | The connected-account type. Must be one that lets a payout be held back; an account that pays out on the provider's own schedule is refused rather than run as protection that only appears to work. |
billing.marketplace.buyer_protection.confirm_after_days | BILLING_BUYER_PROTECTION_CONFIRM_AFTER_DAYS | 14 | How long the buyer's silence takes to become consent. An open dispute stops this clock. |
billing.marketplace.buyer_protection.decide_after_days | BILLING_BUYER_PROTECTION_DECIDE_AFTER_DAYS | 60 | The deadline nothing stops. On a disputed hold it does not decide the dispute — it marks the hold as needing a human. |
billing.marketplace.buyer_protection.provider_limit_days | BILLING_BUYER_PROTECTION_PROVIDER_LIMIT_DAYS | 90 | How long your provider will delay a payout at all. Past it the money goes out whatever the settings say. |
billing.marketplace.buyer_protection.margin_days | BILLING_BUYER_PROTECTION_MARGIN_DAYS | 20 | How much room to leave before that wall. A decision deadline that does not finish inside it is refused. |
billing.marketplace.negative_balance | — | the two keys below | What happens when a merchant owes the platform money — a clawback the provider had nothing left to take. |
billing.marketplace.negative_balance.offset_against_payouts | BILLING_MARKETPLACE_OFFSET_DEBT | true | Whether the next settlement pays the debt down before anything leaves. Offsetting withholds a payment; it is not a reduction of consideration, so the settlement document still states its full amount and no correcting document is due. Switching it off is a commercial choice — the debt then stands as a claim to pursue rather than quietly disappearing. |
billing.marketplace.negative_balance.claim_after_days | BILLING_MARKETPLACE_CLAIM_AFTER_DAYS | 90 | How long a debt may sit untouched before it counts as a receivable to chase. The clock starts when the balance crosses into debt and is cleared when it is settled, so a second debt starts a second clock rather than inheriting the first one's age. |
billing.marketplace.dispute_rate | — | the key below | The line at which a seller's dispute rate needs a look. |
billing.marketplace.dispute_rate.warn_at | BILLING_MARKETPLACE_DISPUTE_RATE_WARN_AT | 0.01 | The warning line as a share of payments: 0.01 is one dispute per hundred payments. DisputeRates::reachesWarningThreshold() compares a rate with it. The default is Mastercard's line for monitoring a seller and sits below the 1.5% at which both networks call a seller excessive, because the package counts disputes and no fraud reports. A value that is not a share above zero and at most one, such as 1.5 meant as a percentage, is refused with an exception. |
billing.marketplace.pricing | — | the key below | How a price behaves across markets with different tax rates. |
billing.marketplace.pricing.mode | BILLING_MARKETPLACE_PRICING_MODE | uniform_gross | Which quantity stays fixed when the rate changes — something has to move. uniform_gross keeps one buyer price everywhere and lets the payout absorb the difference; uniform_payout keeps the payout predictable and moves the price. A creator names their target payout rather than the buyer's price, which is what makes a change in their own tax standing visible instead of leaving the price fixed while the payout moves underneath them. Note that under the default rounding order a target that does not divide evenly is paid one cent short — 50.00 at 15% is paid as 49.99 — so show the resulting payout rather than repeating the request back. |
billing.marketplace.tips | — | the three keys below | Tips and pay-what-you-want, run through the ordinary sale pipeline rather than a donation side path. A tip is consideration for the creator's supply and carries the same regime, commission and document chain. |
billing.marketplace.tips.enabled | BILLING_MARKETPLACE_TIPS | false | Whether tipping is on at all. |
billing.marketplace.tips.commission_bps | BILLING_MARKETPLACE_TIPS_COMMISSION_BPS | null | A tip-specific commission rate in basis points. Setting it replaces the ordinary commission entirely — both the rate and the flat per-transaction component, which becomes zero. That is deliberate: a flat fee is a fixed amount, so on a small voluntary payment it would take a large share of it, and on a small enough tip more than the tip. Null means a tip takes the platform's ordinary commission including that flat component. So the flat fee applies to tips exactly when you have said nothing about tips, and stops applying the moment you set a rate — set it to the same value as your ordinary rate if you want a tip charged like any other sale minus the flat part. |
billing.marketplace.tips.minimum_minor | BILLING_MARKETPLACE_TIPS_MINIMUM_MINOR | null | The floor a buyer-chosen tip must clear, in minor units, refused on the server with FanPriceTooLow — the same refusal a pay-what-you-want price below its floor gets, and for the same reason: an amount the buyer picks is the one place the package's stance against price injection would otherwise lapse. Null inherits pwyw.minimum_minor, which is the honest default — an operator who has said a buyer-chosen amount below X is not worth a transaction has said something the provider's fee makes true of both entries. Set it to allow a voluntary payment lower than a purchase, or to 0 to carry no floor on tips where a sale carries one; null and zero are deliberately different answers. A tip of zero is still no sale rather than a refusal, and is answered before the floor. |
billing.marketplace.tips.line_name | BILLING_MARKETPLACE_TIPS_LINE_NAME | Tip | What the tip line is called on the buyer's hosted checkout page — the one string this package puts in front of a buyer on the provider's site. Configurable because a hardcoded English word would sit on a checkout in any other language, and the default names the act rather than the merchant: whose name belongs on that page is yours to decide, and a provider page is not where this package starts composing sentences about other people. |
billing.marketplace.pwyw | — | the key below | Pay-what-you-want pricing, where the fan chooses the price. |
billing.marketplace.pwyw.minimum_minor | BILLING_MARKETPLACE_PWYW_MINIMUM_MINOR | 0 | The floor a chosen price may not fall below, in minor units. Enforced on the server — a buyer-chosen price is the one place the package's anti-injection stance would otherwise lapse. A chosen amount of zero is refused as no sale regardless of this floor. |
billing.marketplace.buyer_fee | — | the keys below | A fee charged to the buyer on a C2C sale — a separate supply from the seller-side commission, and the platform's own first supply in the intermediary posture. Off by default. Its place of supply is where the mediated sale happens, not where the buyer banks, and it is quoted gross. Kept on its own revenue account, because netting it into the item price or the seller's turnover would make a taxable supply of the platform's own disappear. |
billing.marketplace.buyer_fee.enabled | BILLING_MARKETPLACE_BUYER_FEE | false | Whether a buyer fee is charged at all. |
billing.marketplace.buyer_fee.model | BILLING_MARKETPLACE_BUYER_FEE_MODEL | percent | percent (a share of the sale) or fixed (a flat amount, charged on top of the item price rather than out of it). |
billing.marketplace.buyer_fee.bps | BILLING_MARKETPLACE_BUYER_FEE_BPS | 0 | The percentage rate in basis points, for the percent model. |
billing.marketplace.buyer_fee.fixed_minor | BILLING_MARKETPLACE_BUYER_FEE_FIXED_MINOR | 0 | The flat gross amount in minor units, for the fixed model. |
billing.marketplace.buyer_fee.place_of_supply | BILLING_MARKETPLACE_BUYER_FEE_PLACE | null | The country whose rate the fee carries. It is where the mediated sale happens, never where the buyer banks — a mediation is supplied where the transaction it mediates is, so the payer's own seat does not move it. Unset, the shipped checkout states the sale's currency region, which is what that lane actually knows about the transaction; set it, or supply your own checkout, when you can answer more precisely. |
billing.marketplace.buyer_fee.revenue_account | BILLING_MARKETPLACE_BUYER_FEE_ACCOUNT | 8510 | The revenue account the fee is booked to. The number is config; that it is separate from the item and the seller's turnover is structural. |
billing.marketplace.seller_fee.enabled | BILLING_MARKETPLACE_SELLER_FEE | false | Whether the seller is charged a commission for having their sale arranged — the platform's second intermediation supply, and the mirror of the buyer fee above. Off by default, so a mediated sale still produces exactly the one document it produces today until you turn this on. |
billing.marketplace.seller_fee.model | BILLING_MARKETPLACE_SELLER_FEE_MODEL | percent | percent (a share of the sale) or fixed (a flat amount). Unlike the buyer fee, a fixed commission is capped by the sale: it comes out of the payout, and a fee larger than the sale would owe the seller a negative amount. |
billing.marketplace.seller_fee.bps | BILLING_MARKETPLACE_SELLER_FEE_BPS | 0 | The percentage rate in basis points, for the percent model. |
billing.marketplace.seller_fee.fixed_minor | BILLING_MARKETPLACE_SELLER_FEE_FIXED_MINOR | 0 | The flat gross amount in minor units, for the fixed model, capped by the sale. |
billing.marketplace.seller_fee.revenue_account | BILLING_MARKETPLACE_SELLER_FEE_ACCOUNT | 8511 | The revenue account the commission is booked to. Its own account rather than the buyer fee's, because it is its own supply — booking both to one would make them indistinguishable in an export that has to tell them apart. |
billing.marketplace.charge_type_by_posture | — | the two keys below | Which money routing is compatible with which declared seller. The charge type decides who the provider treats as the merchant of record; the posture decides who the documents name as the seller. Neither determines the other — for electronic services the seller is assigned by law regardless of how the money flows — and because they are independent they can be set to disagree. A pair that disagrees raises no error: it produces a receipt and a settlement describing different transactions, found in an audit. A missing or misshapen table permits nothing rather than everything, so a typo cannot open the combination the check exists to close. |
billing.marketplace.charge_type_by_posture.destination | — | ['seller_of_record', 'platform_intermediary'] | Postures a destination charge may be used with. It makes the connected account the merchant of record, so it fits a sale the merchant is the seller of. |
billing.marketplace.charge_type_by_posture.separate_transfer | — | ['platform_deemed_supplier', 'platform_intermediary'] | Postures a separate transfer may be used with. The platform takes the whole payment and carries the dispute, which is what a platform that is the deemed supplier must do whatever it would prefer. |
billing.marketplace.fee.default_bps | BILLING_MARKETPLACE_FEE_BPS | 0 | What the platform keeps, in basis points — 250 is 2.5%. Zero is the neutral position, deliberately: shipping a take rate would be choosing your commercial terms for you. A value that cannot be read as an integer is refused rather than cast, because the cast of a broken rate is 0, and a zero commission is indistinguishable from a platform that deliberately takes nothing. |
billing.marketplace.fee.default_flat_minor | BILLING_MARKETPLACE_FEE_FLAT_MINOR | 0 | A fixed amount kept per transaction, in minor units. Supported alongside the rate because the provider's own pricing has the same shape, and a marketplace that could express only one would have to approximate the other. Taken first; the rate then applies to what remains. |
billing.marketplace.webhook | — | the two keys below | The endpoint provider events about a merchant arrive on. It is separate from the platform endpoint, with a separate secret. |
billing.marketplace.webhook.path | BILLING_MARKETPLACE_WEBHOOK_PATH | billing/webhook/marketplace | Where merchant events are received. The endpoint answers as the platform endpoint does, 200 for a handled delivery and 404 while the marketplace is off, and it carries the same billing.webhook_middleware. |
billing.marketplace.webhook.secret | BILLING_MARKETPLACE_WEBHOOK_SECRET | null | The signing secret for that endpoint. Required in production once the marketplace is on — the app refuses to boot without it, because a marketplace whose merchant events all fail verification reports no error at all: the capability flags simply never move, and a merchant who lost their payout capability keeps being paid. Never reuse the platform secret: a verifier accepting either would let the merchant key authenticate platform events, and those move the platform's own money. |
billing.marketplace.onboarding | — | the key below | Giving a merchant an account at the provider and driving its hosted identity flow. |
billing.marketplace.onboarding.account_type | BILLING_MARKETPLACE_ACCOUNT_TYPE | express | Which kind of provider account a merchant gets. express leaves onboarding, identity and the merchant dashboard with the provider; standard gives the merchant a full account and a direct relationship with the provider. It decides who runs the identity checks and who absorbs a loss, and a provider will not change it once a merchant has onboarded — so an unsupported value is refused at boot. |
billing.marketplace.preflight | — | the two keys below | The go-live checklist that gates enabled. php artisan billing:marketplace:preflight prints it; with the marketplace on, an open blocking point refuses the boot and names itself. |
billing.marketplace.preflight.attestations | — | [] | Points nobody can check from here — terms published, a registration filed — recorded as your own statement, keyed by checkpoint key: version, attested_at (YYYY-MM-DD, mandatory) and an optional reference. The version is the expiry: when a release changes what has to be attested, the recorded version stops matching and the point goes red until somebody re-reads and re-attests. |
billing.marketplace.preflight.waived | — | [] | Blocking points you deliberately proceed without, by checkpoint key. A waived point is still evaluated and still prints why it did not hold; it is demoted to a warning, never to a pass. Structural points cannot be waived. An entry naming one, a key no checkpoint carries and a value that is not a list of keys are each reported as a blocking failure under configuration.waivers rather than ignored. |
A platform that sets its own terms, authorizes billing or approves the supply cannot truthfully assert the
three no_* keys. Leave them false.
Replacing the package's own models
| Key | Env | Default | What it does |
|---|---|---|---|
billing.models | — | [] | Your subclasses of the package's own models, keyed by the package class. Inert until set; see below. |
models replaces one of the package's own models with your subclass, keyed by the package class:
'models' => [
\Pushery\Billing\Models\Subscription::class => \App\Models\Subscription::class,
],
It is not customer.model, which names your billable model. Use models when you need your own relations, scopes or casts on an order, a subscription or an invoice record. The package then uses your class on every path: every query, every row it writes, and the relations between its models. In your own code, Subscription::model() returns the configured class and Subscription::resolve() a new instance of it.
- Your subclass inherits the table and the guards, and it has to keep both. Several of these rows are append-only records of money and tax, and the package writes them only through its own paths.
- A class that does not exist, or does not extend the package class, is ignored in favor of the package class. Obeying it would fail inside a webhook or a renewal run, long after boot.
- Mapping a model that other rows point at polymorphically changes the type new rows record, unless you register a morph map for it. The package reads both names wherever it compares them, so older rows keep matching.
config/account.php
account and license are this package's names in your application's config. A file of your own called
config/account.php or config/license.php merges with the package's settings, and its values then steer the hub:
an account.middleware of yours, for example, replaces the hub's auth. Keep your own settings under another
name; billing:doctor names every key in either file that the package does not ship.
| Key | Env | Default | What it does |
|---|---|---|---|
account.enabled | BILLING_ACCOUNT_ENABLED | true | Whether the hub is mounted at all. Turn it off when your app has account screens of its own: none of the hub's routes or Livewire screens are registered (the realtime bridge billing.account-realtime stays, for a layout of your own), and a checkout returns to billing.checkout.success_url and billing.checkout.cancel_url, which then have to be set. See The account hub. |
account.prefix | BILLING_ACCOUNT_PREFIX | account/billing | The URL prefix the hub mounts under. The whole hub is gated on billing.enabled, and payments-only mode (billing.mode) leaves it unmounted. |
account.middleware | — | ['web', 'auth'] | The middleware stack the hub routes run through. The hub shows a signed-in owner their own billing. |
account.cancel_requires_confirmation | BILLING_CANCEL_CONFIRM | false | Whether canceling takes two clicks. Off by default: canceling is reversible, and a subscription that is hard to leave is a dark pattern rather than a safeguard. Turn it on when your own acceptance asks for a confirmation step — the first click arms the cancellation, the second carries it out, and there is a way back in between. |
account.layout | BILLING_ACCOUNT_LAYOUT | billing::layouts.account | The Blade layout the full-page screens extend. Point it at your own layout to frame the hub in your chrome. |
account.csp.enabled | BILLING_ACCOUNT_CSP | true | The scoped Content-Security-Policy, so the driver's payment element loads on the billing screens only. Turn it off only if your app already sends its own CSP for these routes — browsers enforce every CSP header at once. |
account.csp.additional | — | [] | Extra origins to allow, keyed by directive (for example 'font-src' => ['https://fonts.example']). Whitelist here rather than turning the header off. |
account.stylesheet | BILLING_ACCOUNT_STYLESHEET | null | A compiled stylesheet for the standalone layout. Unused in the recommended setup, where account.layout points at your own layout and the package's views are added to your Tailwind source scan. A stylesheet on another origin is added to the hub's style-src, so it needs no entry in account.csp.additional. |
config/license.php
| Key | Env | Default | What it does |
|---|---|---|---|
license.tiers | — | [] | What each tier unlocks, keyed by tier key: features (boolean grants) and limits (numeric ceilings, where null means uncapped; a string of digits, which is how env() delivers a number, counts as that number). A tier, feature or limit that is not listed is denied or uncapped by the safe defaults — never an error. |
This file is licensing, billing.php is pricing, and they are orthogonal on purpose. Billing code never
reads license.* — an architecture test enforces it — and the single bridge is the License contract.
Neither ever blocks a public or marketing surface.