Troubleshooting
Every exception the package throws, what it means and what to do about it — plus the failures that throw nothing at all, which are the ones that cost money.
The package is deliberately fail-closed. Where a misconfiguration would otherwise degrade into something that looks like it works — usage counted and never billed, VAT computed and never charged, a webhook accepted without a signature — it refuses instead. That is why several of these fire at boot rather than on the request that would have gone wrong.
The app refuses to boot
Six guards run in the service provider's boot(), all of them behind the billing.enabled master switch:
turn billing off and none of them can stop your app starting.
WithdrawalConsentMissing
A digital work was about to be provided without the consent that extinguishes the right to withdraw.
For a work whose withdrawal right ends on delivery — a download, an e-book — the right ends only if the buyer both agreed to immediate provision and acknowledged the forfeiture, before provision, on the record. Providing it without that recorded consent means every refund inside the window is owed rather than granted. Where the platform is the seller of record, that is the platform's own money.
Capture the two declarations at checkout as a WithdrawalConsent and pass it through to provision. They are
separate statements about different things and neither alone is enough. The gate is only live when
billing.consumer_rights.profile is set; with it off there is no gate and this exception cannot be raised.
FanPriceTooLow
A fan chose a pay-what-you-want price below the floor you set.
The minimum lives in billing.marketplace.pwyw.minimum_minor and is enforced on the server, which is the
point: a price the buyer picks is the one place the package's stance against price injection would otherwise
lapse, so the floor cannot be a client-side check. A chosen amount of zero is refused earlier and separately
as no sale at all, whatever the floor — so this exception is specifically "below a non-zero minimum", not
"nothing was chosen".
MarketNotOpen
A sale was attempted into a country you have not opened.
This one is refused before the payment rather than after it, and the timing is the point: a sale into a country where nobody is registered cannot be repaired by any document written afterwards. The tax has arisen, the registration has not, and the remedies are retroactive registration or a voluntary disclosure.
Set billing.tax_markets to a map of ISO country code to open, planned or blocked. Anything not
explicitly open is refused — including a country the evidence could not resolve, because "we could not
tell where they are" is the clearest reason not to sell somewhere unknown rather than a reason to guess.
The feature is opt-in: with no map configured there is no gate at all. That is deliberate, because a gate defaulting to closed would stop every existing install at its next sale, which is an outage rather than a guard. The exception carries the country and its state so you can word your own message — a buyer should be told their country is not served yet, not shown a tax registration problem.
InconsistentChargeRoutingForPosture
The money is routed one way and the seller is declared to be somebody else.
The charge type and the seller posture are independent on purpose: how a provider moves money says nothing about who the law treats as the seller, and for electronic services the seller is assigned regardless of the money flow. Independent axes can be set to disagree, and a disagreement raises nothing on its own — it produces a receipt naming one seller and a settlement moving money as though it were another. That is found in an audit, not in a log.
Change one of the two. If the platform really is the seller, the payment has to be taken by the platform and
the merchant's share moved separately (separate_transfer). If the merchant is the seller, a destination
charge is right and the posture should say so. The permitted pairs are a table in
billing.marketplace.charge_type_by_posture, so a different legal reading is a configuration change rather
than a code change.
MeteringUnsupported
Tier metering is configured (meter '…'), but the active billing driver '…' cannot report usage.
A tier bills for usage on a driver that has no way to report it. The degraded alternative is the worst one available: every unit counted, none reported, and an invoice for the base fee alone — nothing looks broken until the month's revenue comes in short.
Either move to a driver that reports usage, or remove the metered components from the tier.
TaxModeUnsupported
Three shapes, one theme: the invoice would go out with no tax and nothing would surface it until the VAT return did not add up.
billing.taxisprovider, but the active driver computes no provider tax.billing.taxis a local mode such aseu_oss, but the active driver defers tax to the provider — the VAT would be computed locally and never charged.billing.taxis not a resolvable mode at all: a typo (eu_os), or the key turned into an array by adding a sub-key underneath it.
Set billing.tax to a mode the driver can actually apply, or to none.
InvalidBillingConfig
The configuration contradicts itself in a way that would fail silently at runtime:
| Message | Cause |
|---|---|
billing.owner must be 'user' or 'team' | Any other value |
billing.zero_tier is '…', but no tier with that key is defined | The fail-safe tier does not exist, so a fallback would land nowhere |
billing.untouchable_tiers lists '…', but no tier with that key is defined | A protected tier key that does not exist protects nothing |
Tier '…' references dimension '…', which is not defined | The usage screen would render a dimension nothing feeds |
The price currency '…' is not a valid ISO 4217 code | Not three uppercase letters |
Dimension '…' warn_threshold must be between 0 and 1 | A threshold outside that range never warns, or always does |
The dunning ladder's after_days must strictly ascend | Rungs out of order never escalate |
RetentionBelowStatutoryMinimum
billing.retention.erased_financial_days is … days, below the ~10-year statutory floor …
An erased owner's retained invoices would be pruned before the law allows. Keeping data longer is always
fine — only shortening it below the floor is refused, and only until someone opts in on purpose for a
jurisdiction whose minimum genuinely is shorter. Raise the window, or set
billing.retention.allow_below_statutory_minimum deliberately.
CustodyModeNotPermitted
billing.marketplace.custody.platform_held is on, which means the platform itself would hold other people's
funds. That is a regulated activity in most jurisdictions, so a configuration flag alone is never enough:
bind an implementation of Pushery\Billing\Contracts\PaymentServiceLicenseAttestation to declare in code
that you hold the license, or turn the flag off.
MarketplaceUnsupported
The active billing driver [...] does not route money to merchants.
billing.marketplace.enabled is on, but the active driver cannot send a payment anywhere except the
platform's own account. A driver announces that it can by implementing
Pushery\Billing\Contracts\RoutesMoney; one that does not is refused here rather than at the first sale,
because a marketplace that reads as enabled and settles every charge to the platform looks completely
healthy until someone reconciles the money. Use a driver that routes, or leave the switch off (the default)
and sell as a single seller.
The same exception is thrown at call time when something asks for the marketplace rails anyway — including
when billing.enabled is false, where the no-op driver is active and the marketplace path does not exist
at all. The two cases carry different messages because their fixes differ.
ReceiveEligibilityDenied
The merchant is not eligible to receive money
Money was about to be destined to a merchant the receiving gate refuses, and it is thrown BEFORE any provider call. That ordering is the whole value: once a routed charge is in flight, a merchant who cannot receive does not produce a clean rejection — the money settles wherever the provider can reach, usually the platform, while the local records say it was split, and unwinding that is manual and per transaction.
The usual cause is that the provider has not confirmed all three capabilities for that merchant: they can take charges, they can receive payouts, and they have finished submitting their details. Those are the provider's to grant, arrive asynchronously, and can be withdrawn again — so a merchant who worked yesterday can be refused today. Check the merchant's account row and when it was last refreshed.
It is deliberately not the same exception as EligibilityDenied, which refuses the BUYER. The two are
refused for unrelated reasons and fixed by different people.
CreatorTaxStatusUnclear
No settlement document may be produced while the merchant's tax standing is unestablished
There is no safe guess here, and that is the whole reason for the refusal. Assume the merchant charges tax normally and the document states tax a small business does not owe — at which point the recipient owes it merely because a document says so, unless they object in time to a document they never asked for. Assume the opposite and the document understates a real liability and forfeits a deduction.
The two errors point in opposite directions, so neither default is the cautious one. Not producing the document is.
Record a tax standing for the merchant, and the hold lifts on its own. It cannot be lifted any other way — there is no override and no key naming a default standing, because either would be the silent default this exists to prevent.
ProductNotClassified
This product has no archetype
Or: a voluntary payment was resolved without naming what it was paid on. Both refuse for the same reason — there is no safe substitute for the missing answer.
Every consequence of a sale follows from what kind of thing was sold: where it is taxed, at which band, whether it is reportable, what the buyer may undo. A missing classification is therefore not one unknown but five guesses, and guesses that happen to be right most of the time are the hardest defects to find — nothing fails, the numbers look ordinary, and only the minority of sales where the guess was wrong are wrong.
Classify the product before it becomes sellable. For a voluntary payment, name what it was paid on: it takes its treatment from there, and defaulting would under-report a tip on reportable work — or, guessing the other way, report one that is not reportable, which is its own offense rather than a cautious error.
RegimeNotPermitted
The supply regime [...] is not one this platform has opted into
Or: it contradicts the seller posture. Both mean the same thing — a sale was about to be classified in a shape you have not signed up for.
A regime decides which documents a sale produces and whose turnover it is, so falling into one is never
acceptable. Add it to billing.marketplace.regime.allowed only when you mean it, and set
billing.marketplace.regime.default to a value that exists — an unreadable one is refused rather than
defaulted, because silently choosing here would pick which documents every sale produces on the strength of
a typo.
The contradiction case is stricter and worth understanding. The regime and the seller posture are one decision seen twice: the regime is how the books read, the posture is who the receipt names. A pair that disagrees would issue a receipt and a settlement document describing different transactions — each internally consistent, and comparable only by somebody who thought to compare them. So reselling in your own name pairs with the deemed-supplier posture, arranging somebody else's sale pairs with the intermediary posture, and naming the merchant as the seller has no regime at all in the shipped profile: it is neither of the two shapes, so no document chain follows from it. A jurisdiction where it does binds its own profile.
TaxDisclosureNotPermitted
A settlement document to a creator whose standing is [...] may not state tax
A self-billed document was about to state tax for a creator whose standing does not permit it. Only a validated, standard-rated domestic creator may be shown tax on a document the platform writes for them — the German profile's whitelist. For every other standing (a small business, a business abroad, a private individual, one still awaiting registry validation, or one never established) the document is issued with no tax statement, and a document that already states no tax always passes.
This is the strictest lock in the settlement chain, and for a concrete reason: a self-billed document that wrongly states tax makes the RECIPIENT owe that tax (§ 14c Abs. 2 UStG), so a classification slip would hand a creator a tax bill for a document they never wrote. The standing is read at the supply date, not when the document is generated, so a retroactive correction cannot rewrite the tax on a past supply. Reach it by letting the tax matrix decide the variant rather than setting a tax amount yourself; the whitelist lives in the jurisdiction profile, so a consumer elsewhere admits their own permitted standings and the guard is unchanged.
SelfBillingAgreementMissing
No active self-billing agreement authorizes a document for creator [...] on a supply dated [...]
A self-billed document was about to be issued for a creator who has no agreement authorizing it. A self-billed document is an invoice only if both sides agreed to the arrangement before the supply — one issued without that agreement carries no input-tax effect and cannot be healed, so the write is refused rather than left to produce a worthless document.
The check is strictly ex ante: an agreement accepted after the supply does not reach back to cover it, and
a revocation dated after the supply leaves that supply covered because the arrangement was live when it
happened. Record the creator's agreement — its accepted_at, the terms version, and a proof protocol —
before settling them, and re-issue any document that was refused once the agreement exists. A jurisdiction
that does not require a prior agreement sets billing.marketplace.self_billing.require_agreement to false,
but never rely on that implicitly: the default, and a missing or non-boolean value, keep the requirement.
SellerContradictsPosture
The seller-of-record posture [...] makes the platform the seller, but the document names a different party
The seller a document snapshots does not agree with its frozen seller-of-record posture. The posture is the role, the seller is the party that role resolves to, and they are one decision seen twice: under the deemed-supplier posture the platform is the seller toward the buyer, and when the platform only arranges the sale (or the merchant sells in their own name) the merchant is.
Naming the merchant as seller under a deemed-supplier posture would put a creator in front of the buyer as the seller — the exact outcome the deemed-supplier rule exists to prevent — so the write is refused at creation, for either direction of the mismatch. Snapshot the party the posture calls for: the platform company for the deemed supplier, the merchant otherwise. A document that snapshots no seller at all is a single-seller one and never reaches this check.
SelfBillingDisabled
The self-billing engine was reached while billing.marketplace.self_billing.enabled is off
The self-billing engine was called to settle a creator, but self-billing is switched off. A platform that does not self-bill routes its creators to the fallback lane — the creator submits their own invoice — and never settles them through the engine, so reaching it with the switch off is a caller mistake, not a document to produce.
Check billing.marketplace.self_billing.enabled before settling and route to the fallback lane when it is
off, or turn it on if the platform does self-bill. The engine refuses loudly here rather than issue a
document a disabled platform never meant to.
MerchantPartyUnavailable
No merchant party resolver is bound, so the identity of a [...] merchant cannot be read for a self-billed document
A self-billed document was about to be issued, but nothing knows how to read the merchant's invoice identity — their legal name and registered address. A self-billed document names the merchant as the seller, so it cannot be issued without that, and a merchant's details live in your application's own schema, not in this package. The shipped resolver therefore fails closed rather than issue a document with no seller.
Bind a MerchantPartyResolver that reads your merchants before self-billing — it maps one of your merchant
models to an invoice Party. A single-seller install never reaches this: it does not self-bill.
MarketplaceNotReadyForGoLive
billing.marketplace.enabled is true, but the go-live checklist has open blocking points
The marketplace switch is on while php artisan billing:marketplace:preflight still reports something open.
The switch is tied to the checklist rather than to your memory of it, because skipping the checklist leaves
no trace: the marketplace comes up, sells, and books everything under a configuration nobody signed off.
The message lists every open point by key and reason — it has to, because a refusal at boot has also taken
away the command that would have explained it. Switch the marketplace back off, run the preflight, close the
points, and switch it on again. A point whose obligation genuinely does not apply to you goes into
billing.marketplace.preflight.waived, where it stays visible as a warning instead of disappearing.
UnknownJurisdictionProfile
billing.tax_profile is set to [...], which is not a jurisdiction profile this package ships
The configured profile name is neither shipped (de) nor bound in the container. This is refused rather
than ignored: falling back to "no profile" would give you a checklist that quietly omits every obligation
you asked it to enforce and still reports green. Use a shipped name, bind your own
Pushery\Billing\Contracts\JurisdictionProfile, or set the key to null.
DuplicateGoLiveCheckpoint
Two go-live checkpoints are registered under the key [...]
A checkpoint you registered uses a key that is already taken. Neither resolution would be safe — the later
winning would let a passing point silently replace a blocking one, the earlier winning would drop your point
— so it is raised where it happens. Rename yours, or, if you meant to switch the existing point off, leave
it in place and add its key to billing.marketplace.preflight.waived.
WebhookSigningNotConfigured
Webhook signature verification for the 'stripe' driver is not configured.
Production only. Outside production the guard is silent, so this is a deployment failure, not a local
one. Set the driver's signing secret — for the Stripe driver, STRIPE_WEBHOOK_SECRET, which Cashier reads
into cashier.webhook.secret. Booting without it would mean accepting unverified webhooks, and a webhook is
how money and entitlements move.
Runtime errors
BillingDisabled
Billing is disabled; cannot charge.
billing.enabled is false, so the manager resolved the NullDriver — and something asked it to move
money anyway. Reading is fine while billing is off; charging, refunding, tokenizing a payment method and
creating a mandate are not. Guard the call site, or turn billing on.
UnsupportedDriver
billing.default names a driver nothing registered. Check the spelling, and that the driver's service
provider is installed.
EligibilityDenied
The owner is not eligible to transact; money movement was refused.
The CanTransactMoney gate denied the owner before a charge, subscribe, checkout or add-on purchase. The
gate is fail-closed: it denies unless the owner is positively eligible, so an unbound or
not-yet-answering implementation denies rather than lets money move on an unanswered question. Bind your own
implementation if you have age or identity requirements; if you do not, make sure the one you bound returns
true for an ordinary owner.
QuotaExceeded
Quota exceeded on meter '…': N requested, M remaining in the allowance.
A metered request would take the owner past a blocking allowance. A degrading or fair-use meter never
raises this — those keep serving and are only flagged. The exception carries meterKey, policy and
remaining, so catch it and render "you have M left" rather than a bare status code. The
billing.quota:<meter> middleware turns it into billing.quota.status (429 by default) for you.
SeatDowngradeBelowOccupied
The seat quantity would be set below the number of seats actually occupied, which would bill for fewer seats than are in use. Remove members first, or bill the higher number.
CouponUnavailable
Four reasons, all recoverable and all worth showing the customer verbatim: the coupon is not active, it has expired, it has reached its redemption limit, or this account has already redeemed it. Catch it at the redemption call site and surface the message.
CurrencyMismatch
Cannot operate on Money of different currencies: … vs … .
Two Money values in different currencies met in an arithmetic operation. This is a programming error
rather than a configuration one: the value object refuses instead of producing a number whose currency is a
guess. It also fires when a tier's catalog price is in a different currency from the subscription line it
would price.
CycleAmountUnresolvable
A subscription line cannot be priced for its cycle. Four causes:
- a fixed line carrying no amount — only a metered line may be stored unpriced
- a metered line with no resolver named, on the line's
preprocessorcolumn or bound as a default - a metered line whose meter has no matching component in the tier catalog
- a metered component with no unit price, on a driver that rates usage locally
It throws rather than returning zero, because a metered line with no usage legitimately costs nothing: a zero for "I could not work this out" produces an invoice that looks settled while billing nothing.
PostureNotPermitted
Either the resolved seller-of-record posture is not in
billing.marketplace.seller_of_record.allowed_postures, or a seller_of_record posture was resolved for an
electronically supplied service without the rebuttal being asserted. A platform that sets its own terms,
authorizes billing or approves the supply cannot truthfully assert the rebuttal.
Tax and invoicing
UnknownTaxCountry
The tax country code '…' is not an assigned ISO 3166-1 alpha-2 country code.
Pass a two-letter code (DE, US). It is refused rather than zero-rated because a zero here is
indistinguishable from a legitimate supply outside the EU VAT area, and would under-declare VAT silently.
Worth knowing what this cannot catch: a typo that lands on another assigned country (DE mistyped as
DK) is indistinguishable from a deliberate supply to that country.
InvalidInvoiceCorrection
- An amendment must reference the invoice it corrects. A correction with no origin reference is only valid as a cancellation.
- A correction carries positive magnitudes. The document's nature inverts the meaning, not the sign, so pass the absolute amount being corrected.
Both are thrown when the snapshot is constructed, so a malformed correction never reaches persistence or an e-invoice writer.
DatevTransactionUnresolvable
No DATEV account is configured for the '…' transaction.
Configure the account under billing.datev.accounts for the active chart. The export aborts rather than
booking to a default account, because a posting on the wrong account imports cleanly and surfaces only when
an auditor reads it.
PdfRendererUnavailable and MissingPdfEmbedder
The package renders invoice HTML itself and leaves the PDF step to you, because a PDF toolchain is a heavy, opinionated dependency a lean package should not force on every install.
PdfRendererUnavailable— a PDF was requested and noPdfRendereris bound. Bind one, or use the HTML.MissingPdfEmbedder— a hybrid ZUGFeRD PDF/A-3 was requested without the optional embedding toolchain. Install it, or use the CII XML directly.
Nothing threw, and something is still wrong
These are the expensive ones, because the app looks healthy.
A paying customer still sees the free tier. billing.customer.model is not set, so no subscription
webhook can resolve its owner and the local row is never written. Set it, then run billing:sync to backfill
what the webhooks could not apply.
Usage is recorded and never billed. The meter does not exist at the provider, or was archived there.
Usage reported into a meter that does not exist fails silently and surfaces, if ever, as an under-charged
invoice a month later. Run billing:meters:check, which exits non-zero on a missing meter and fits a deploy
check. Then billing:usage:reconcile --redrive returns the rollups that gave up to pending.
Usage stopped flowing after an outage. The flusher exits successfully during a provider outage on
purpose — a growing backlog is not a crash. Watch for the UsageBacklogStalled event, which fires once the
oldest pending rollup is older than billing.metering.stall_hours, and for UsageReconciliationDrift, which
fires when the local ledger and the provider's meter disagree.
Webhooks arrive but nothing happens. Check the delivery rows: an effect that failed all its retries is
marked failed and stays re-driveable. billing:webhooks:replay --failed runs it again long after the
provider stopped redelivering. Idempotency is per effect, so a replay re-runs only the effect that failed.
The account hub 404s. Either billing.enabled is off — the routes are not registered at all — or
Livewire is not installed. The hub is optional and Livewire is a suggested dependency, not a hard one; the
billing core does not need it.
The admin console denies everyone. billing.admin.ability names a Gate ability your app defines.
Until it is defined the Gate denies, which is deliberate: the console is never open by accident.
A cancellation at the provider did not stop billing here, or the other way round. Dispatch
BillableAccountDeleting before you delete a billable model. The listener runs while the owner still
exists, so the live subscription can be canceled at the provider before the row is gone.
Everything is fine locally and broken in production. The webhook-secret guard is production-only, and
tax and metering guards depend on the driver that is actually active. Run with the production driver and
APP_ENV=production once before deploying.