Skip to main content

Event reference

The package's own events, all under Pushery\Billing\Events. Listen to these and your app never reads a provider payload.

A driver's webhook mapper translates each provider event — a signed Stripe event, a bare-id ping, an HMAC batch — into one of the domain events below, and everything downstream listens on those. That is what makes a side effect survive a driver change: the mapper is provider-specific, the event is not.

The marker interface is BillingDomainEvent. It carries no methods; it exists so the effect bus can only be handed an event that is part of this contract.

Listening​

Every domain event goes through Laravel's dispatcher, so listen the ordinary way:

use Illuminate\Support\Facades\Event;
use Pushery\Billing\Events\PaymentFailed;

Event::listen(PaymentFailed::class, function (PaymentFailed $event): void {
// $event->customerReference, $event->amount, $event->reference
});

They are equally ordinary to assert on:

use Illuminate\Support\Facades\Event;
use Pushery\Billing\Events\SubscriptionStateChanged;

Event::fake();

// ... exercise the code under test ...

Event::assertDispatched(SubscriptionStateChanged::class);

See Testing for Billing::fake(), which fakes the driver rather than the events, and for the cross-engine suites.

The events​

customerReference is always the provider's customer id, resolved back to your billable model through billing.customer.column. Money is always a Money value object — an integer minor amount plus a currency, never a float.

callerReference is your key, carried through the purchase and given back untouched. Pass whatever identifies the row you wrote before opening the checkout — a cart id, an order number, sponsorship:4711 — and read it back here. This package attaches no meaning to it and never looks inside it.

It exists because the other key that travels, declarationReference, is minted by PurchaseDeclarations::declare(), which records a withdrawal consent while it mints. A business buyer has no right of withdrawal, so there is nothing to declare and no reference to mint. Without a key of its own, exactly the purchases without a declaration would arrive with no key at all. For a one-off a session id could stand in; for a subscription nothing could, because the subscription reference does not exist yet when the session is opened.

Do not send a correlation id as declarationReference instead. It would come back meaning this buyer declared, which is the one statement a business checkout must not make. The two are separate keys with separate meanings, and a checkout may carry both.

subscriptionType names which contract the event is about, and it is null for the ordinary case of one subscription per owner. A local row is unique per owner, type and merchant, so an owner who holds two contracts at the same merchant tells them apart by this field alone — pass the same type you passed to subscribe(), and read it back here. Null is read as the default type wherever none is named, which is why nothing moves for a consumer that never uses a second contract.

couponCode on SubscriptionStateChanged is the local code of a merchant coupon this package minted — the one in your own coupons row, not the provider's id — so booking the redemption against that row needs none of the provider's arithmetic. Its null is as meaningful as its value. A catalog coupon, created by a human at the provider with its own max_redemptions, is counted by the provider itself: there is nothing for you to book, so the field is null. It is also null when no discount was applied. Read the value as "this redemption is yours to count" and the null as "it is not" — counting on a coupon id instead would make the two cases look alike and have you count a redemption twice.

endsAt on SubscriptionStateChanged is when a canceled subscription ends, in Unix seconds, and once it has ended, when it did. It is not the period end. An ordinary cancellation ends there, but a cancellation to a date ends inside the period, and reading periodEnd instead would tell the owner they keep access for weeks they no longer have. It is null while nothing ends the subscription.

EventWhen it firesPayload
SubscriptionStateChangedA subscription moved to a new canonical statecustomerReference, state (a SubscriptionState), subscriptionReference?, tierKey?, occurredAt?, periodStart?, periodEnd?, trialEnd?, merchant?, merchantAccountReference?, declarationReference?, startedAt?, couponCode?, subscriptionType?, callerReference?, endsAt?
TrialEndingA trial is about to end, a few days outcustomerReference, subscriptionReference, trialEndsAt
PaymentSucceededA payment completedcustomerReference, amount, reference
PaymentFailedA payment attempt failedcustomerReference, amount, reference
AccountPaymentChangedA one-off payment into a payment account changed, as its provider reported it on the account's webhook address. It carries where the payment stands now, never a delta, and it is the only event an account's webhooks raiseprovider, account, paymentReference, status, amount, amountRefunded, amountChargedBack, billingReference?, metadata, refundReference?
PaymentActionRequiredThe bank asked the cardholder to confirm (3-D Secure)customerReference, reference
PaymentReminderDueA marketplace subscription is in arrears and still inside its cure window — today's reminder is dueentry (an ArrearsEntry), daysLeft, subscription?
SubscriptionExpiredA marketplace subscription ran out its cure window and expired for goodsubscription, accessEndsAt
WebhookDeliveryRefusedA delivery reached a billing webhook endpoint and its verifier refused itprovider, surface, path, userAgent?
WriteOffRecoveredMoney arrived against a receivable written off as uncollectible — the write-off was a judgment the future disagreed withcorrection, received, paymentReference
ChargebackReceivedA chargeback was decided against a settled paymentcustomerReference, reference, amount, merchantReference?, transferReference?, feeAmount?, cause?, reason?, disputeReference?
ChargebackReversedA chargeback the bank took back, so the money is with the merchant again. Raised by billing:money-back:reconcile for a provider that reports a chargeback before the bank decidescustomerReference, reference, amount, disputeReference, reversedAt?, standingRefunded?
DisputeOpenedA buyer disputed a payment and the case can still be answered. Raised when the provider opens it, long before any outcomedisputeReference, paymentReference, amount, reason, reasonCode?, evidenceDueBy?, accountReference?, merchantReference?
InPersonSalePaidA buyer paid a sale at the counter with a card, and the provider confirmed it. The moment the sale is documented: until the card was presented nothing had been soldprovider, paymentReference, amount
InPersonSaleCanceledA sale was taken off the reader before it was paid, and the provider closed its payment. A declined card does not raise it, because that payment stays open on the readerprovider, paymentReference
MandateRevokedA stored mandate is no longer usablecustomerReference, mandateId
DefaultPaymentMethodChangedThe payment method a customer is charged with by default changed, or its details did: set or removed in the provider's portal, or replaced by the bank's card updater. It names the customer, because the method is read againcustomerReference
CustomerDeletedThe provider deleted the customer, so its reference names nothing any morecustomerReference
MandateEstablishedA payment method was granted and can now be charged off-session. A card mandate carries its card where the provider names itcustomerReference, mandateId, provider, method?, paymentReference?, card? (a PaymentMethod)
PaymentMethodCollectedA customer finished the hosted page for adding a payment method. It carries the provider's handle for the collection, not the method, because the notification that the page was completed does not name the methodcustomerReference, collectionReference, provider
AddonPurchasedA one-time add-on was bought and paidcustomerReference, addonKey, amount, reference, paymentReference?, declarationReference?, buyerCountry?, taxMinor?, provider?, callerReference?, buyerTaxId?
AddonRefundedA charge was refundedpaymentReference, cumulativeRefunded, reason?, standingChargebacks
RefundFailedA refund will not reach the customer: it failed at the provider, or it was canceled before it was paid out. Raised by billing:money-back:reconcile (Mollie) and by the refund webhooks of Stripe, which name no customercustomerReference, paymentReference, refundReference, amount, failure (a RefundFailure: failed or canceled), reason?
FanTipPaidA buyer-chosen tip was paid on a hosted checkout page. Its own event rather than an add-on purchase, because a tip has no catalog key — read as one it would be sent to the grant path, which would look up a key that does not exist and credit nothingcustomerReference, amount, reference, paymentReference?, soldAlongside, merchantReference, buyerIsDomestic?, declarationReference?
InvoiceFinalizedThe provider finalized an invoice — it now legally existscustomerReference (the provider's customer the invoice is for), invoice (an InvoiceSnapshot)
SaleCountryReportedThe provider reported the country it taxed a sale in: a settled one-time checkout, and every invoice of a subscription, once finalized and again when paidcustomerReference, saleReference, country?, amount, paid, chargeReference?, subscriptionReference?
TaxIdVerificationReportedThe provider reported what a tax authority's register said about a buyer's tax ID: pending first, a verdict once the register answers. A checkout checks only the format while the buyer is on the pagecustomerReference, provider, taxIdReference, type, value, status, verifiedName?, verifiedAddress?
InvoiceCorrectedA correcting document was issued against a finalized invoicecustomerReference (the provider's customer the correction is for), correction (an InvoiceCorrectionSnapshot)
InvoiceCreditedDeprecated — the former name of InvoiceCorrectedcustomerReference, correction
MerchantAccountUpdatedThe provider reported what a merchant's account can doaccount (a MerchantAccountReference), occurredAt?
MerchantTransferReversedMoney came back from a merchant — a refund or a lost disputemerchant, provider, chargeReference, amount, feeReturned, cause, disputeFee?
MerchantReversalCameBackShortLess came back from a merchant than the reversal asked for; the package books nothingmerchant, provider, chargeReference, shortfall, cause
MerchantAccountDeauthorizedA merchant disconnected their account from the platformprovider, accountReference
MerchantTransferReversedByProviderThe provider reversed a transfer this platform did not ask it to reverse. It arrives on the platform endpoint, where Stripe sends the events of the platform's transfersprovider, transferReference, amountReversedMinor
MerchantPayoutFailedThe provider could not move a merchant's money from their connected balance to their bank. A different question from a transfer, and it fails on its own terms — a wrong IBAN, a closed account, a bank that refuses it — with nothing wrong about the transfer that fed it. Only the failure is carried: the success is the ordinary case and the provider's dashboard already shows it. Attributed to the merchant, never to a charge, because a payout bundles many transfers and has no 1:1 relation to a recorded saleprovider, accountReference, payoutReference, amountMinor, currency, failureCode?, failureMessage?
BuyerProtectionHoldReleasedThe protection period ended in the seller's favor and the money is on its way to themchargeReference, merchantType?, merchantId?, amount, state
BuyerProtectionHoldRefundedThe protection period ended in the buyer's favor and the money goes back to themchargeReference, merchantType?, merchantId?, amount, state
BuyerProtectionResolutionRequiredNobody decided in time and this package will not decide for them — a disputed hold past its deadlinechargeReference, merchantType?, merchantId?, amount, state
MerchantRoutingSuspendedThe platform stopped routing new money to a merchantprovider, accountReference, reason
MerchantRoutingReinstatedA suspended merchant may receive againprovider, accountReference
MerchantTerminatedThe relationship ended and cannot be resumed from hereprovider, accountReference, reason
RoutedChargeConfirmedA routed payment that could not settle at once has cleared. Two cannot: a card demanding 3-D Secure, and a bank debit that clears days later — both return successfully having moved no money, so the merchant's row is written pending until this arrives. A hosted one-off sale is pending until the buyer pays, and its payment carries the sale's own reference.provider, paymentReference, transferReference?, saleReference?
RoutedChargeAbandonedA pending routed payment will not complete. Only ever from pending: a charge that settled and later goes wrong is a refund or a dispute, never a failure, and calling it one would erase the fact that the money was really there. Also raised when a hosted checkout lapses unpaid.provider, paymentReference, saleReference?
MerchantShareNotMovedA paid sale's merchant share failed to move. On a separate transfer the share moves in a second call after the buyer's payment, and that call failed: the buyer has paid, the merchant has not been, and the sale's row stays pending with the failure written beside it. No webhook ever arrives for a transfer that was never created, so this is the only signal. billing:marketplace:retry-transfers moves the share once the cause is fixed, and a retry that fails again dispatches it againmerchant, provider, chargeReference, amount, reason
CreatorTaxStatusChangedA merchant's tax standing movedmerchant, previous, current, effectiveFrom, source
CreatorReattestationDueA creator's tax attestation is due for renewal: once when the year turns, once more shortly before it runs out (lastReminder). holdFrom is the day selling and payouts pause if nothing is renewed. The package delivers ReattestationDueNotification from it; replace that listener to route the notice your own waymerchant, holdFrom, lastReminder
SettlementRestatedA settlement was canceled and issued again because its creator's standing was corrected. The package moves no money: the replacement usually pays a different amount, and difference() says by how much, positive when the creator is owed moreoriginal, cancellation, replacement
SellerDataReminderDueA seller's record is still missing something and they are asked for it, once per reminder (reminder). missingFields names what is asked for; measureFrom and measure are set only where a measure can follow. The package delivers SellerDataReminderNotification from it; replace that listener to route the notice your own waymerchant, episodeId, reminder, missingFields, measureFrom?, measure?
SellerDataMeasureChangedA measure over missing seller data began (inForce) or ended, with the reason it ended: record_completed, no_longer_required or withholding_exhausted. A suspension is also announced by the merchant lifecycle's own eventsmerchant, measure, inForce, reason?
DeemedSupplyReceiptWithheldA supply of goods the platform is treated as making itself was paid for on the provider's page, and the buyer's document was not issued, because the payment contradicts how the sale was classified before the session opened: the buyer entered a tax ID on the page, the destination is outside the Union, or the provider computed a tax at another rate than the one the supply is taxed at. The money has moved; have somebody refund the sale or document by hand what really happenedbuyer, provider, chargeReference, supply (a DeemedSupplyOfGoods), buyerTaxId?, reason
SellerDeclarationContradictedSomething the application reported contradicts what a seller outside the Union declared, so the platform can no longer rely on that declaration (Art. 5c Implementing Regulation 282/2011). The seller's next sale of goods the platform would supply itself waits until the seller has declared again; ask the seller tomerchant, contradiction (a SellerDeclarationContradiction)
CreatorPlacedOnTaxHoldA merchant can no longer sell or be paid out. Dispatched for both routes into a hold — somebody recording a blocking standing, and an attestation quietly expiring. The second writes nothing at all, so a scheduled sweep finds it; without both, the event's silence would read as "no hold". Fires once per hold.merchant, reasonKey (a translation key, never a sentence — why a standing blocks is a jurisdiction's rule)
SaleIntoClosedMarketReversedA sale the provider taxed in a market billing.tax_markets does not open was undone: the subscription ended, then the payment refunded. Tell the buyer their country is not served yet. A refund the provider refused arrives with refund->successful false, and the buyer is still owed the moneyowner, country (unknown when none was reported), state, saleReference, subscriptionEnded, refund?
TaxIdVerificationFailedA buyer's tax ID turned out not to be registered. The charge was reversed on the number's format, so a sale under it can owe the tax that was not charged. reverseChargeInvoices lists this owner's invoices issued with the charge reversed under that number, and it can be empty for a one-off checkout without an invoice recordowner, type, value, reverseChargeInvoices
FilingObligationApproachingA filing obligation falls due inside the notice window, and this is the one warning it gets. Dispatched per obligation, never per day: the last period's return and the annual seller report share the end-of-January deadline, and a single "something is due" would let whoever handles the one they thought of consider the day dealt with. Two events for one date is the intended shape.obligation, dueOn, period? (null for the annual report, which covers a year), daysRemaining
VoucherVolumeThresholdApproachingVoucher volume is close to the figure at which a supervisory filing is expected. A warning exists because a threshold you learn about on the day you cross it leaves no time to act, and registering with a supervisor has a lead time only the operator can start. The package announces; it notifies no authority and holds credentials for none.volume, threshold, observedAt
VoucherVolumeThresholdBreachedVoucher volume has passed that figure. Deliberately a separate event from the warning above rather than a level on one: a recipient treating them as one message would let the early notice stand for the late one, and only the late one has a deadline attached. The figure travels with the event because the window is rolling — what triggered it is not what a recipient would compute later.volume, threshold, observedAt
RoutedSubscriptionInvoiceDraftedA renewal of a subscription was invoiced as a draft, the hour in which a routed cycle's commission can still be set. Its own event for the reason RoutedSubscriptionInvoicePaid is one: the handler reaches the provider, and the subscription reference lets it ask the local row first and stop there.customerReference, invoiceReference, subscriptionReference
RoutedSubscriptionInvoicePaidA subscription cycle was invoiced and paid — the moment a routed subscription's commission becomes a fact. Its own event beside PaymentSucceeded on purpose: the handler reaches the provider, so hanging it on the payment would mean provider calls for every payment on every install, including the great majority that route nothing. The subscription reference is what lets the handler ask the local row first and stop there.customerReference, invoiceReference, subscriptionReference
InvoiceUpcomingThe provider is about to finalize the next invoicecustomerReference
SeatQuantityChangedA team's billed seat quantity actually movedowner, from, to
SubscriptionPausedAn owner's subscription was paused, once the driver accepted itowner
SubscriptionUnpausedA paused subscription was taken up again and is collected from nowowner, pausedSeconds?
UsageBacklogStalledUsage has sat unreported longer than billing.metering.stall_hourspendingRollups, pendingUnits, oldestRecordedAt, stalledHours
UsageReconciliationDriftOur ledger and the provider's meter disagreeowner, meterKey, period, reported, recorded
UsageSpendCapReachedThe owner's spend cap stopped a metered request, or usage recorded past it took the spend over it; once per billing period, and once more after a raiseowner, cap, spent, period
UsageSpendCapRaisedAn owner's spend cap was raisedowner, from, to
ProviderJournalDriftThe merchant journal and the provider disagree about money that already moved to a merchantmerchant, chargeReference, transferReference, reason, ours, theirs?
BillableAccountDeletingAn account is about to be deletedowner
AccountBillingUpdatedAn owner's billing state changed — a broadcast, for live refreshowner
AccountToastNotifiedA transient message for the owner — a broadcastowner, message, level

A few of these repay a closer look.

ProviderJournalDrift compares one transfer, never a balance. The obvious check — the package's figure against the connected account's balance at the provider — fires on every legitimate event, because that balance also moves when the merchant is paid out to their bank, when they take money through another integration, and when the provider debits a fee. An alarm that fires on all of those is switched off within a week, and on a money path a muted alarm is worse than none: it is also an alibi. So the comparison is per transfer, the one object both sides name and only this package creates. A destination charge's transfer carries the whole payment, and the provider collects the platform's fee back from the merchant afterwards, so the provider's side is the transfer less the part of that fee the platform still holds.

Which side is right is not symmetric, and the event's job is only the first half of the question. The provider is authoritative for what moved; the package is authoritative for what was owed. So a drift is repaired by correcting the local row — never by transferring again to match the journal, which would move real money to settle a bookkeeping disagreement. billing:merchants:reconcile reports and exits non-zero; it repairs nothing, deliberately.

SubscriptionStateChanged carries more than the state. occurredAt is the provider event's own timestamp, which is how a retried or out-of-order delivery is ignored instead of regressing a newer state. periodStart and periodEnd ride along because metered usage is accounted into the subscription's cycle, not a calendar month — an owner who renews on the 31st has no calendar month to bill into. Each is null when the provider conveys no such value, and a null tierKey means the change conveys no tier.

AddonRefunded carries the cumulative total, not the delta. The add-on ledger claws back only the part it has not already reversed, so two partial refunds and a redelivery each do the right thing. It is keyed on paymentReference — the payment id, not the checkout session — because that is what a refund event carries. A refund of anything that is not a tracked add-on matches no purchase and reverses nothing.

Under Mollie, RefundFailed and ChargebackReversed come from a daily reconciliation, not from a webhook. Mollie calls no webhook for a refund canceled before it was paid out and documents none for a chargeback the bank reverses, and the webhook of a payment is read for its refunded total, which says nothing about a refund that failed. billing:money-back:reconcile reads the provider's refunds and chargebacks back, records each outcome once in the delivery ledger, and raises the event through the same effect bus as a webhook. A listener hears of each outcome once; one whose effects did not all finish is dispatched again by the next run. A failed refund acts on nothing by itself: it leaves a customer who was told the money is on its way, and it is written to the log as well, so an install that listens for nothing still finds it. A reversed chargeback gives back the access the chargeback ended (RestoreAccessOnChargebackReversal, switched by billing.content_ownership.restore_on_chargeback_reversal), and restores the add-on purchase it reversed to standingRefunded, what the payment still has refunded or charged back (RestoreAddonPurchaseOnChargebackReversal): the credit comes back on the balance, or exactly the units the clawback took. A refund that still stands stays reversed. A credit note against an invoice this package raised stays, because it is a numbered document, and ReinstateLocalInvoiceOnChargebackReversal charges what it credited again on an invoice of its own: from the invoice series, naming the invoice it charges again, and booked and counted like any invoice in the period of the reversal. A new chargeback of the same amount afterwards acts again: AddonRefunded names the chargebacks its total counts (standingChargebacks), so the effects tell it apart from a redelivery of the first.

Under Stripe, RefundFailed comes from the webhook instead: refund.failed, or refund.updated and charge.refund.updated with a failed or canceled refund. Its reason is Stripe's failure_reason, and it names no customer, because a refund carries none.

BillableAccountDeleting is present-continuous on purpose. The listener runs while the owner still exists, so the live subscription can be canceled at the provider before the row is gone. The package's own eraser dispatches it; an app with its own delete flow dispatches it itself, after re-confirming identity and before deleting the model. Skip it and a deleted account lingers as an active, still-charging subscription.

WebhookDeliveryRefused is a security signal, and its payload is deliberately thin. It carries the request's facts and never the body: the delivery did not verify, so the body is attacker-controlled input, and putting it on an event invites it into logs, queues and audit tables sized for trusted data. It also carries no reason — not which part of the signature failed, because that is a probing oracle, and not a coarse reason field either, since WebhookVerifier answers yes or no and a field with one possible value describes nothing while looking like it describes something.

And it carries no network address. This package hands an address to exactly one place — the argument of the IpCountryResolver you bind — and reads one nowhere else, because a second path is where an address leaks. An event is the worst of those paths: it travels into queued payloads, audit tables and exception context. If you want the caller's address in your own audit trail, take it from the request inside your listener; keeping it is then your decision, in your privacy notice, on your storage.

The dispatch is guarded, so a listener that throws cannot turn the endpoint's deliberate 400 into a 5xx that tells the sender to retry a request which can never become valid. The package also writes a warning to the log, so an install that wires no listener still sees the attempt. Nothing is recorded in the delivery ledger: that table holds provider payloads for replay, and filling it from unauthenticated traffic would let anybody who can reach the URL write rows into it.

UsageBacklogStalled is the alarm a successful exit code cannot raise. The flusher exits successfully during a provider outage, because a growing backlog is not a crash — but a backlog that never drains is lost revenue, since past the provider's acceptance window the usage is not retro-billed at all.

Deprecation aliases​

InvoiceCredited was renamed to InvoiceCorrected: the old name conflated a correcting document with a self-billing credit note, which is a different document with a different type code.

For one deprecation window the old class still fires. InvoiceCorrected implements HasDeprecatedAlias, and the effect bus dispatches the alias through Laravel's dispatcher alongside the event — so an existing Event::listen(InvoiceCredited::class) keeps being called instead of going quiet, which is the worst outcome of a rename. The alias reaches host listeners only and is never re-run through the package's own effects, so nothing is persisted twice.

Migrate to InvoiceCorrected and read $event->correction. The old class and the alias firing go in a later release.

The shipped effects​

An effect is an invokable class registered against a domain event, and each runs in its own queued job — so a slow or failing effect can neither hold the provider's request open nor take its siblings down with it. Idempotency is per effect, not per delivery, so replaying a delivery whose third effect failed re-runs only that one.

Each run holds a transaction around its effect, and no effect calls a provider inside it. A credit mirrored onto the provider's balance, a payment method made the default, a reversal or a refund goes to a further job that is queued once the run has committed, and a usage flush or a late fee follows the commit in the same job. A run that rolls back leaves nothing at the provider behind it.

EventEffectWhat it does
PaymentSucceededReopenWriteOffOnLateReceiptRaises WriteOffRecovered when a payment matches exactly one reopenable write-off — never on an ambiguous match
SubscriptionStateChangedSyncPlanFromSubscriptionWrites the local subscription row and the owner's tier column
SubscriptionStateChangedSendSubscriptionActivatedNoticeTells the owner the subscription is live, once per subscription
SubscriptionStateChangedSendSubscriptionCanceledNoticeTells the owner when access ends, keyed on the grace state and on endsAt, or the period end when the cancellation named no date
PaymentSucceededSendPaymentReceiptTells the owner their money moved
PaymentSucceededSettleCycleOnPaymentCloses a billing cycle whose charge settled after the run that started it had finished, and advances the period. A bank debit is accepted at once and clears days later, so the cycle is held open until this arrives. Registered only where the package runs the billing cycle itself; a provider that runs its own answers through its own events
PaymentFailedFailCycleOnPaymentThe other direction of the same hold: a debit that was accepted and then bounced becomes a real failure, and dunning starts then rather than when the payment was created. Without it a bounced debit would leave the cycle held with no charge, no ladder and no notice
PaymentFailedSendDunningNoticeStarts or advances the dunning conversation
PaymentActionRequiredSendPaymentActionRequiredNoticeNudges the owner to confirm at their bank
AddonPurchasedCreditAddonPurchaseApplies the credit or the prepaid units, exactly once per purchase
AddonPurchasedGrantPurchasedContentWrites the ownership row when the thing bought was a work; the shipped content map answers "not a work" to everything, so it writes nothing until you say otherwise
AddonPurchasedRecordTheDestinationOfARoutedSaleWrites the country the buyer entered on the provider's page onto the routed sale's row, once, which the platform's records of a sale it only arranged read as its destination. A country already on the row is kept
AddonPurchasedTakeHostedPurchaseCommissionOnNetBrings the commission of a routed purchase on the provider's hosted page down to the net. The fee was fixed when the session opened, before the buyer's rate existed, so at a tax-inclusive price it was taken on the buyer's tax as well. Where the buyer paid exactly the price the session recorded and the provider computed tax inside it, the rate is read through the same seam the purchase's document reads, and TakeCommissionOnNet gives the difference back as part of the fee after the commit and brings the row to the net. Silent on an unrouted purchase, a separate transfer, a sale with no tax, a tax-exclusive or discounted price, and a tax the provider cannot state as one rate
AddonPurchasedInvoiceLocalPurchaseCloses the order a local engine wrote when its checkout opened, and raises the invoice from it, once the provider confirms the payment. Mollie issues no invoice of its own, so this is where an add-on bought there gets one. Silent where no such order is in flight: a Stripe purchase, a cycle's order, and a redelivery, which finds the order already closed
AddonPurchasedIssueDocumentForRoutedHostedPurchaseIssues the buyer's document for a routed sale paid on the provider's own hosted page. The synchronous routed lane ends in a document and this one did not, so a supply happened and nothing said so. Silent on an unrouted purchase, on a posture where the merchant is the seller of record, on a buyer this application does not own, on an add-on nobody classified, and on a sale whose tax the provider already computed — a positive tax figure means the rate came from the buyer's address, and this package holds amounts rather than rates. A supply of goods the platform is treated as making itself gets the document of that supply, with its tax decided from the consignment its row kept, or DeemedSupplyReceiptWithheld where the payment contradicts the classification. Once per charge reference, however often the provider redelivers
RoutedSubscriptionInvoicePaidRecordRoutedSubscriptionChargeWrites the merchant-charge row for the cycle, one per invoice. A routed subscription is priced with a rate, so its commission exists once per cycle and only at the provider — this is the one effect that reads from one. A destination cycle's share moved with the payment, so its row is settled as it is written and its transfer is asked for after the webhook commits; one whose fee was withheld on the invoice total, the buyer's tax included, also has the commission on the tax given back after the commit, through TakeCommissionOnNet. A separate-transfer cycle is priced from the terms frozen on its subscription and stays pending until its share moves, in a job after the webhook commits. An unrouted subscription stops at the local row and reaches nothing
RoutedSubscriptionInvoiceDraftedTakeRoutedCycleCommissionOnNetSets a routed renewal's commission on the net before the provider charges it. A destination subscription states its rate as a percentage of the invoice total, the buyer's tax included, and a draft still takes an absolute fee: the fee is set on the net the invoice's own tax rate separates and noted in the invoice's metadata, and the paid cycle's row takes that rate only where the fee withheld is that fee. A first cycle is paid at checkout with no draft, and a draft charged before the fee was set keeps the percentage until TakeCommissionOnNet gives the commission on the tax back after the payment; an invoice that charged no tax or names more than one rate keeps it for good. An unrouted subscription stops at the local row and reaches nothing. With the marketplace enabled, the platform's endpoint has to receive invoice.created
AddonRefundedReverseAddonPurchaseClaws back the unspent part of the purchase
AddonRefundedRevokeAccessOnRefundEnds access to the refunded work, if your install says a refund should end access — on by default and genuinely switchable, because both answers are somebody's policy
AddonRefundedIssueLocalCreditNoteIssues the credit note for an invoice this package raised itself. A provider that issues its own documents announces the correction too, and that path is handled by PersistInvoiceCorrection; a local engine has nobody to announce it, so without this a refunded cycle leaves the books overstating turnover. It acts only where the refunded payment leads to a locally raised invoice, so it is silent on a provider-issued one. Amounts are positive — a correcting document's type inverts the accounting direction, not a minus sign — and the credited total is capped at the invoice it corrects. The refunded gross is stated as net and tax at the rate of the invoice it corrects, so the return reduces both; a note against an invoice that states no tax states none either. What an invoice has been credited is net of an invoice that charged a credited amount again after a reversed chargeback, so a later refund credits the right difference.
InvoiceFinalizedPersistInvoiceWrites the immutable invoice record the e-invoice and DATEV exports render from
InvoiceFinalizedDebitCreditAppliedByProviderTakes credit the provider spent on this invoice off this package's ledger. A driver with a customer balance mirrors credit onto the provider so it reduces the next invoice by itself; without this the mirror points one way only, and the local balance — the one the account hub shows and a locally collected charge spends from — stays too high from that invoice on. Runs after PersistInvoice, because the movement is keyed on the invoice record that effect writes, and debits once however often the provider redelivers. Silent where the invoice consumed nothing, which is also every invoice on a driver that holds no provider balance.
SaleCountryReportedReverseSaleIntoClosedMarketUndoes a sale taxed in a market that is not open: ends the subscription, refunds the payment and raises SaleIntoClosedMarketReversed, in a job that is queued once the run has committed. Inert until billing.tax_markets is configured, and a sale without consideration is left alone unless it starts a subscription
TaxIdVerificationReportedRecordTaxIdVerificationKeeps each answer as a row of its own, retained and unlinked on erasure, and raises TaxIdVerificationFailed for a number the register does not know. Once per tax ID and answer, so a repeated report adds nothing
InvoiceCorrectedPersistInvoiceCorrectionWrites the correction row, linked to the invoice it corrects
InvoiceUpcomingFlushUpcomingUsageForce-flushes the usage outbox so a closing cycle is billed in that cycle
MandateRevokedRevokeMandateDrops the stored mandate so nothing is charged against it
DefaultPaymentMethodChangedMirrorDefaultPaymentMethodReads the customer's default method from the provider once the run has committed and copies its brand and last four digits into the owner's pm_type and pm_last_four, or clears them when there is no default. An owner whose table has neither column is left alone. Registered by the Stripe driver
CustomerDeletedForgetDeletedCustomerClears the owner's customer reference (billing.customer.column) and the card copy, so the next checkout creates a new customer instead of failing on a deleted one. Registered by the Stripe driver
PaymentMethodCollectedAdoptCollectedPaymentMethodMakes the collected method the default, for the customer and for every subscription that names a method of its own, so the next charge and the provider's next retry take it. The hosted page only attaches the method, and a subscription opened through a checkout is charged with the card it was paid with before the customer's default is ever read. Registered by a driver whose hosted page leaves a new method merely attached, and silent for an owner this application does not know
MandateEstablishedStoreMandateStores the granted mandate so the engine can charge it, with the card's brand, last four digits and expiry when the event carries one, so the expiring-card warning can read them; idempotent, and it claims default only when the owner holds none
MandateEstablishedStartSubscriptionOnMandateTurns a pending subscription intent into the subscription it was for. Registered only by a driver whose mandates arrive this way, and matched on paymentReference — a customer merely adding a second card grants a mandate too, and matching on the customer would hand them a subscription they never asked for
TrialEndingSendTrialEndingNoticeReminds the owner before the first charge
MerchantAccountUpdatedRefreshMerchantCapabilitiesStores the reported capabilities, unless the report is older than the one they came from — the only path by which one ever moves
MerchantAccountDeauthorizedMarkMerchantDeauthorizedStamps when the platform lost reach, keeping the first date
MerchantTransferReversedByProviderRecordProviderTransferReversalRecords the provider's cumulative reversal onto the sale, never lowering a figure already booked
MerchantPayoutFailedRecordFailedMerchantPayoutWrites a merchant.payout_failed entry on the merchant's own audit trail — the answer to "I have not been paid". Idempotent by the payout id, so a redelivery adds nothing, while a second failed payout is recorded on its own. An account with no merchant on file is left alone
RoutedChargeConfirmedSettleRoutedChargeOnConfirmationSettles the matching pending routed charge, found under the payment or under the hosted sale's own reference, which it then replaces with the payment; an event naming a payment this package did not route does nothing
RoutedChargeAbandonedSettleRoutedChargeOnConfirmationFails it instead, and only from pending
InPersonSalePaidDocumentInPersonSaleMarks the sale paid and issues its receipt, with the tax decided when it went onto the reader: a simplified invoice up to billing.card_present.small_amount_threshold_minor, a payment record above it. Refuses a payment that collected another amount than the sale went up with. A redelivery issues nothing a second time, and a payment the counter path did not put up is left alone
InPersonSaleCanceledCloseInPersonSaleCloses a pending sale without a receipt. A paid one keeps its receipt
DisputeOpenedRecordOpenedDisputeKeeps the case, once per dispute and whatever its outcome, so DisputeRates can read the rate the card networks measure. It sends nothing to the provider
ChargebackReceivedRecordProviderFeeWrites the provider's dispute fee as its own row, once per dispute — it is an inbound supply, never a deduction
ChargebackReceivedRevokeAccessOnChargebackTakes back the access the disputed payment bought, on your configured coupling
ChargebackReceivedCorrectChainOnChargebackIssues the correcting documents the lost dispute owes — both legs where the supply was undone, the buyer's leg only where the creator delivered and the payment was disputed as fraud
ChargebackReceivedClaimChargebackClawbackClaims the merchant's share back and hands the provider call to a queued job — an effect cannot reach a provider from inside the transaction it runs in
ChargebackReversedRestoreAccessOnChargebackReversalGives back the access the chargeback ended, and only that, once the bank takes the chargeback back. On by default and switchable with billing.content_ownership.restore_on_chargeback_reversal
ChargebackReversedRestoreAddonPurchaseOnChargebackReversalRestores the add-on purchase the chargeback reversed to what the payment still has refunded or charged back, and gives back its credit or exactly the units the clawback took. The credit note is answered by ReinstateLocalInvoiceOnChargebackReversal
ChargebackReversedReinstateLocalInvoiceOnChargebackReversalCharges again, on an invoice of its own, what an invoice this package raised itself was credited for the chargeback. The invoice comes from the invoice series, carries the original's amounts and lines when it charges the whole again and one line naming them otherwise, names the original in BT-25 and on the page, and states the date of supply. What the payment still has refunded or charged back stays credited. Silent on a provider-issued invoice; without standingRefunded it issues nothing and logs a warning

Register your own the same way the shipped ones are registered, against the event rather than against a provider string:

use Pushery\Billing\Events\PaymentSucceeded;
use Pushery\Billing\Webhooks\WebhookEffectRegistry;

app(WebhookEffectRegistry::class)->on(PaymentSucceeded::class, NotifyAccounting::class);

Events with no shipped effect — SeatQuantityChanged, SubscriptionPaused, SubscriptionUnpaused, UsageBacklogStalled, UsageReconciliationDrift, ProviderJournalDrift, BuyerProtectionHoldReleased, BuyerProtectionHoldRefunded, BuyerProtectionResolutionRequired — are dispatched for your app to act on. A reconciliation drift in particular is a decision the package will not make for you.

ChargebackReceived is not among them: four effects are registered against it, in the table above, and the money side is one of them. ClaimChargebackClawback takes the merchant's share back, so an app that reverses its own payout entry should do it from MerchantTransferReversed, which says what actually moved, rather than from the chargeback itself, which only says one was decided.

Two things are worth knowing before adding your own effect to this event. Registering a second fee recorder double-books the dispute fee. And a second effect that reverses the merchant's share reverses it twice — the provider idempotency key belongs to the attempt row this package writes, and a second claim gets a second key the provider has never seen.

Answering a dispute​

DisputeOpened arrives when Stripe opens a case, so you hear about it while the case can still be won. evidenceDueBy is the last moment Stripe accepts evidence. The package records the case and sends nothing on its own: whether a case is worth answering, and with what, is your decision, and you answer through SubmitsDisputeEvidence with the two references the event carries.

use Illuminate\Support\Facades\Event;
use Pushery\Billing\Contracts\SubmitsDisputeEvidence;
use Pushery\Billing\Enums\DisputeDocument;
use Pushery\Billing\Events\DisputeOpened;
use Pushery\Billing\ValueObjects\DisputeEvidence;

Event::listen(function (DisputeOpened $dispute): void {
if (! app()->bound(SubmitsDisputeEvidence::class)) {
return; // the active driver takes no evidence through its API
}

$evidence = new DisputeEvidence(
productDescription: 'Annual membership, billed yearly',
accessActivityLog: 'Signed in 14 times after the purchase, last on 2026-09-18',
)->withDocument(DisputeDocument::Receipt, storage_path("receipts/{$dispute->paymentReference}.pdf"));

app(SubmitsDisputeEvidence::class)->submit($dispute->disputeReference, $evidence, $dispute->accountReference);
});
  • Submitting is final. Stripe decides the case on what arrived, and a submitted case cannot be amended. The same evidence submitted again is answered by Stripe as the first submission, so a retried job cannot answer a case twice. Evidence that says nothing is refused, because an empty submission gives the case up.
  • Choose the evidence by reasonCode. reason folds every code the package does not act on into Unknown; reasonCode is Stripe's own. A product_not_received case is answered with the shipping fields, a duplicate with duplicatePaymentReference and its explanation, a subscription_canceled with the cancellation fields.
  • Documents are local files the submitting process can read. Each is uploaded to Stripe as a dispute document on the account the dispute lives on, and a path that cannot be read fails when the evidence is built, before anything reaches Stripe.
  • On a marketplace, a dispute on a merchant's own charge lives on the merchant's account, and accountReference names it. A routed sale whose payment the platform took whole is answered on the platform; merchantReference still says whose sale it was, because the evidence is usually theirs.
  • Your Stripe endpoint has to send charge.dispute.created, and the Connect endpoint too where merchants take their own charges. An endpoint that sends only charge.dispute.closed reports the outcome and never the opening.

Mollie takes no dispute evidence through its API, so under Mollie nothing is bound and no case is raised.

Queued listeners​

A listener that implements ShouldQueue receives its event after a round trip through the queue. Every event that carries a model stores it there by its class and key, as Laravel's SerializesModels does: the listener reads the row back when it runs, and neither the queue nor failed_jobs holds the model's attributes. What an event reports, such as a status before and after, an amount or a date, travels as a value and arrives as it was dispatched. A row deleted in between fails the job with ModelNotFoundException, unless the listener sets $deleteWhenMissingModels = true or carries the DeleteWhenMissingModels attribute, which deletes the job instead.

BillableAccountDeleting is the exception, because the account is gone by the time a queued listener runs. Its owner arrives as an instance of the owner's class that carries only the key, enough to clean up what your application keeps under it. A listener that needs the owner's other attributes runs synchronously.

Broadcast events​

AccountBillingUpdated and AccountToastNotified are broadcast on the owner's private channel so the account-hub screens refresh live. Both are a no-op unless billing.realtime.enabled is on and a broadcaster is configured; without that the screens fall back to a bounded poll.

The channel is billing.{morph class}.{key}, and the package authorizes it itself: a signed-in user may listen on it only when their billing owner, resolved through BillingEntityResolver the way the hub resolves it, is the owner the channel names, and only while realtime is on. You do not register a rule for it in routes/channels.php. A rule of your own that also matches billing.* and is registered first is asked first, so do not write a wider one.

AccountBillingUpdated deliberately carries no payload — the client re-fetches — so nothing sensitive is on the wire. A queued broadcast of either event holds the owner as its key and reads the row back when it runs, so neither the queue nor failed_jobs stores the owner's attributes.

The package dispatches both of them itself, from the webhook spine:

BroadcastRaised whenLevel
AccountBillingUpdatedthe plan sync applied a provider event that actually moved something — a state, a tier, a period—
AccountBillingUpdateda paid add-on purchase was recorded, on the delivery that recorded it—
AccountToastNotifieda payment faileddanger
AccountToastNotifieda subscription went livesuccess
AccountToastNotifieda paid add-on was credited to the account or granted its units, naming the amount or the countsuccess

Two things follow from that, and both are deliberate.

A redelivery that changes nothing broadcasts nothing. Providers retry freely, and an event that moved no row must not make every open screen re-fetch.

The toast message arrives as a finished sentence in the owner's own language, resolved at dispatch against the owner's stored locale rather than the ambient one. A broadcast is raised from a webhook, where there is no request and therefore no locale but the application default — so unlike a notification, which Laravel localizes for you, a toast has to be translated where it is raised. The strings live under billing::account.toast.* and are publishable like the rest.

The severity changes key on its way to the browser​

The broadcast payload is { message, level }. The headless bridge in the account hub receives it and re-dispatches it to the browser as a wirekit-toast event whose payload is { message, variant } — the same severity, under the name a toast region reads. The bridge is <livewire:billing.account-realtime />: the hub's layout mounts it, and an application with account screens of its own mounts it in its own layout. It stays registered with the hub switched off.

That rename is the one thing to know if you write your own listener: read event.detail.variant, not detail.level. An unrecognized key here does not fail loudly. It falls through to the default severity, so a failed payment renders in the neutral style and is announced to a screen reader without urgency — wrong in both channels, and visibly wrong in neither.

Something has to be listening, and by default that is not this package​

The bridge dispatches the event. It does not render toasts, and neither does anything else here unless you ask for it. Where the message actually appears is one of three answers, and choosing none of them is a silent no-op: the broadcast goes out, the event fires, and nothing is drawn.

  1. A WireKit host. WireKit renders its own toast region and reads exactly this event. Nothing to do.

  2. This package's minimal region. Set billing.realtime.render_toast_region to true (BILLING_REALTIME_TOAST_REGION=true). 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. No UI kit, no build step. Leave it off on a WireKit host, or every toast appears twice.

  3. Your own listener, which is one line:

    window.addEventListener('wirekit-toast', (event) => {
    yourToaster(event.detail.message, event.detail.variant);
    });

event.detail.variant is always one of info, success, warning, danger — the bridge clamps an unknown or missing severity back to info rather than trusting the wire. event.detail.message is whatever your application broadcast, so render it as TEXT.


← Back to the documentation index