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.

EventWhen it firesPayload
SubscriptionStateChangedA subscription moved to a new canonical statecustomerReference, state (a SubscriptionState), subscriptionReference?, tierKey?, occurredAt?, periodStart?, periodEnd?, trialEnd?
TrialEndingA trial is about to end, a few days outcustomerReference, subscriptionReference, trialEndsAt
PaymentSucceededA payment completedcustomerReference, amount, reference
PaymentFailedA payment attempt failedcustomerReference, amount, reference
PaymentActionRequiredThe bank asked the cardholder to confirm (3-D Secure)customerReference, reference
ChargebackReceivedA charge was disputedcustomerReference, reference, amount
MandateRevokedA stored mandate is no longer usablecustomerReference, mandateId
AddonPurchasedA one-time add-on was bought and paidcustomerReference, addonKey, amount, reference, paymentReference?
AddonRefundedA charge was refundedpaymentReference, cumulativeRefunded, reason?
InvoiceFinalizedThe provider finalized an invoice — it now legally existsinvoice (an InvoiceSnapshot)
InvoiceCorrectedA correcting document was issued against a finalized invoicecorrection (an InvoiceCorrectionSnapshot)
InvoiceCreditedDeprecated — the former name of InvoiceCorrectedcorrection
MerchantAccountUpdatedThe provider reported what a merchant's account can doaccount (a MerchantAccountReference)
MerchantTransferReversedMoney came back from a merchant — a refund or a lost disputemerchant, provider, chargeReference, amount, feeReturned, cause, disputeFee?
MerchantAccountDeauthorizedA merchant disconnected their account from the platformprovider, accountReference
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
CreatorTaxStatusChangedA merchant's tax standing movedmerchant, previous, current, effectiveFrom, source
InvoiceUpcomingThe provider is about to finalize the next invoicecustomerReference
SeatQuantityChangedA team's billed seat quantity actually movedowner, from, to
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
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.

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.

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.

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.

EventEffectWhat it does
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
PaymentSucceededSendPaymentReceiptTells the owner their money moved
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
AddonRefundedReverseAddonPurchaseClaws back the unspent part of the purchase
InvoiceFinalizedPersistInvoiceWrites the immutable invoice record the e-invoice and DATEV exports render from
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
TrialEndingSendTrialEndingNoticeReminds the owner before the first charge
MerchantAccountUpdatedRefreshMerchantCapabilitiesStores the reported capabilities — the only path by which one ever moves
MerchantAccountDeauthorizedMarkMerchantDeauthorizedStamps when the platform lost reach, keeping the first date

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 — ChargebackReceived, SeatQuantityChanged, UsageBacklogStalled, UsageReconciliationDrift — are dispatched for your app to act on. A chargeback and a reconciliation drift in particular are decisions the package will not make for you.

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.

AccountBillingUpdated deliberately carries no payload — the client re-fetches — so nothing sensitive is on the wire.


← Back to the documentation index