Skip to main content

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.tax is provider, but the active driver computes no provider tax.
  • billing.tax is a local mode such as eu_oss, but the active driver defers tax to the provider — the VAT would be computed locally and never charged.
  • billing.tax is 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:

MessageCause
billing.owner must be 'user' or 'team'Any other value
billing.zero_tier is '…', but no tier with that key is definedThe fail-safe tier does not exist, so a fallback would land nowhere
billing.untouchable_tiers lists '…', but no tier with that key is definedA protected tier key that does not exist protects nothing
Tier '…' references dimension '…', which is not definedThe usage screen would render a dimension nothing feeds
The price currency '…' is not a valid ISO 4217 codeNot three uppercase letters
Dimension '…' warn_threshold must be between 0 and 1A threshold outside that range never warns, or always does
The dunning ladder's after_days must strictly ascendRungs 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 preprocessor column 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 no PdfRenderer is 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.


← Back to the documentation index