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.
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, 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. |
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. |
billing.stripe.api_version | BILLING_STRIPE_API_VERSION | null | The dated Stripe API version every call and webhook runs against. Null uses the version the package pins and is tested against. Override it only deliberately: Stripe versions the shape of a webhook payload, so a newer version can silently change what a mapper reads. |
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 returns the customer and posts its status pings. 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, the driver takes the legacy path: the ping is unsigned and the authentication is the fetch, since an attacker cannot invent a status Mollie will confirm. Set it, and an unsigned ping is refused — you have said your account signs, so an unsigned request is either a misconfiguration or somebody knocking. |
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.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, which reconciles the subscription onto the local row so a paying customer is never shown the free tier. |
billing.checkout.cancel_url | BILLING_CHECKOUT_CANCEL_URL | null | Where an abandoned checkout returns. Null uses the plan screen. |
billing.checkout.portal_return_url | BILLING_PORTAL_RETURN_URL | null | Where the hosted billing portal returns. Falls back to success_url, then the subscription screen. |
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. |
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. |
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. |
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 | int | What YOUR models are keyed by: int, uuid or ulid. Every column referencing one of them is declared with this type when its table is created. 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. |
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 the plan-sync webhook effect never flips, in either direction. Use it for an admin-comped grant a provider event must not overwrite. |
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. |
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, 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, and an optional grants (meter plus units). An add-on grants either money credit or prepaid usage units. |
billing.coupons | — | [] | Package-owned discount codes keyed by the code the customer enters: either percent (1 to 100) or amount plus currency, with an optional expires_at. Add stripe_coupon to have the provider own the money math at checkout. |
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 | — | 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. |
billing.metering.backoff_seconds | — | 60 | Base delay for the exponential backoff between flush attempts. |
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.usage.hold_seconds | BILLING_USAGE_HOLD_SECONDS | 900 | How long a hold on a metered allowance stands before it is handed back. Longer than your slowest metered request, shorter than you would tolerate an owner being short of allowance they never spent. |
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. 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. 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. Ten of the eleven notices carry a call-to-action button, and each links into the account hub — 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. |
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.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.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.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. The screen and the swap read this one value, 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. 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. |
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.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). |
Tax and the invoice surface: Taxes · Invoices and e-invoicing.
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.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. |
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.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 — inclusive by default. A statute exempting whoever "does not exceed" the figure exempts the seller sitting exactly on it, and a strict comparison would report somebody the law leaves out. Change it only if your statute reads differently. |
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 — which matters because a re-render years later resembles the document your customer holds without being it. Left null, no disk is ever touched and the route behaves exactly as before. A recorded path whose file is gone still renders, so an owner is never locked out of their own invoice, but 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 a local engine mints: PREFIX-YYYY-0000001. The running part restarts each year, which is what keeps a number readable. A provider-driven driver never reaches this — it copies the number its provider already issued. Gaps in the sequence are harmless; a number issued twice is not, so the sequence locks rather than counting rows. |
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. 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.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 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. |
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. |
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 judgement 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 also decides where a merchant-defined tier's price is minted — platform_deemed_supplier on the platform account, the other two on the merchant's connected account — because that is the same question, and a second switch could be set to contradict this one. |
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.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 under and its money comparison is inclusive. At exactly the figure they disagree, deliberately — the exemption's boundary is set by law, and moving it to match a preference over-reports. |
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_record | — | the key below | What a platform asks a seller for at onboarding. |
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 returns it in proportion to what the buyer got back — the only value under which a refund nets to zero across all three parties. 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.consumer_rights | — | the two 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. Null means off: no extra checkout step, no changed receipt, byte-identical. 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. 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.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. 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, because a sale into a country with no registration cannot be repaired by any later document. A market opened here that the local rates cannot price refuses the boot. 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. |
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.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 two 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.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. |
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, and an entry naming one is reported as a failure 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.
config/account.php
| Key | Env | Default | What it does |
|---|---|---|---|
account.prefix | BILLING_ACCOUNT_PREFIX | account/billing | The URL prefix the hub mounts under. The whole hub is gated on billing.enabled. |
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. |
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 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.