Skip to main content

Webhooks

Point your provider at the configured billing.webhook_path (default billing/webhook). Deliveries are verified by signature, de-duplicated on the event id, and dispatched to registered effects. The shipped effects sync the owner's plan, credit a one-time add-on exactly once, send a dunning notice, and persist each finalized invoice.

Where a one-time add-on's credit lives depends on the driver. Where the provider keeps a customer balance, the credit is mirrored onto it, so the provider applies it against the next invoice by itself. Where it does not, the credit stays in this package's own ledger and the cycle spends it here. Either way the customer sees the balance in the account hub; what differs is who subtracts it.

The mirror points both ways, and it has to. When the provider applies some of that balance to an invoice, a shipped effect takes the same amount off this package's ledger against the persisted invoice record, under its own reason so a support agent can tell credit the package decided to spend from credit it learned had been spent. Without the return leg the local balance only ever grows: the account hub keeps showing money the provider already consumed, and a locally collected charge spends it a second time. The debit is keyed to the invoice record, so a redelivered event does not take it twice.

A driver that keeps no customer balance is untouched by this, and the reason is the first paragraph: it never mirrors anything out, so there is nothing to hear back. On that lane the package raises the invoice and spends the credit itself, which it already records — an effect that also fired there would be the same double subtraction with its sign flipped.

The boot-time secret guard is the hosted driver's, and deliberately not every driver's. Where the provider signs every delivery, running without the signing secret in production is a silent failure — every event fails verification, so plan changes simply stop arriving — and the package refuses to boot rather than let that run. Where a provider still sends unsigned deliveries, refusing to boot would lock out every install that has no secret to set: there the secret is optional, and an unsigned ping is authenticated by fetching the resource back from the provider. Mollie is that provider, and its classic payment ping stays unsigned even on an account with next-generation webhooks. Setting the secret makes the driver check every signature that arrives and refuse an unsigned next-generation event, while the classic ping is still accepted and fetched back. See billing.mollie.webhook_secret in the configuration reference.

Each effect runs as its own queued job, so one that throws no longer takes the ones after it down with it, each retries on its own, and each leaves a record of what it did or still owes. A raw-payload ledger keeps every delivery so a failed effect can be re-driven with billing:webhooks:replay --failed, rather than depending on the provider to redeliver (which it stops doing after its own retry window). A delivery posted as a form, as Mollie posts its payment pings, is kept as its form fields, and the replay reads them the same way.

An effect that already did its work runs again only where it can. The replay skips a run that is done, so a delivery replayed twice does not credit or mail twice. An effect that finds what an earlier run produced and stops there implements Pushery\Billing\Webhooks\RepeatableEffect, and the replay runs it again when you name it with --rerun: after a fix to what the effect reads, a run that did nothing does its work. --type selects deliveries by their event type, which the replay accepts only together with --rerun, and --limit (100 by default) caps a run:

php artisan billing:webhooks:replay --type=checkout.session.completed --since=2026-09-25 --limit=1000 \
--rerun='Pushery\Billing\Webhooks\Effects\IssueDocumentForRoutedHostedPurchase'

Every other effect of a selected delivery still meets its finished run and is skipped, and an effect that does not implement RepeatableEffect is refused before anything is replayed.

What the endpoint answers. A delivery that passed verification and was recorded gets 200 OK with an empty body, and so does one that mapped to nothing. Mollie reads every other status as a failed delivery, 204 No Content included, and sends it again; Stripe accepts any 2xx. A delivery that fails verification gets 400, and the endpoint answers 404 while billing is disabled. A provider call that fails while the delivery is mapped, a rate limit or an outage, ends in 500, so the provider delivers the event again. Middleware you name in billing.webhook_middleware runs on the endpoint as well, a throttle per address for instance. The package ships none, because it reads no network address; Stripe and Mollie answer a 429 by sending the delivery again.

A classic Mollie ping is unsigned, so it is authenticated by fetching the payment it names back from Mollie. The driver sets its webhook URL on payments only, so a ping has to name one, tr_ followed by letters and digits; any other id is refused with 400 before anything is recorded.

Some changes never reach the endpoint. 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. Where the driver can read refunds and chargebacks back, the service provider schedules billing:money-back:reconcile daily: it records each such outcome once in the same delivery ledger, under the provider's id and the outcome (rfd_…:failed, chb_…:reversed), and raises RefundFailed or ChargebackReversed through the effect bus. The row keeps the ids, the status and the amount. billing:webhooks:replay maps nothing for these rows; an outcome whose effects did not all finish is dispatched again by the next reconciliation.

Which events a Stripe endpoint has to receive. Subscribe the endpoint to all events, or at least to every type below. A selection without one of them loses what hangs off it without a word, because an event that never arrives produces nothing: without invoice.upcoming, for instance, usage reaches the invoice a cycle late. billing:doctor reads your endpoints at Stripe and names every type an endpoint of this package does not receive, and fails while one is missing.

EndpointEvent types
billing.webhook_path, the platform's owncharge.dispute.closed, charge.dispute.created, charge.refund.updated, charge.refunded, checkout.session.async_payment_succeeded, checkout.session.completed, checkout.session.expired, credit_note.created, customer.deleted, customer.subscription.created, customer.subscription.deleted, customer.subscription.trial_will_end, customer.subscription.updated, customer.tax_id.created, customer.tax_id.deleted, customer.tax_id.updated, customer.updated, invoice.finalized, invoice.payment_action_required, invoice.payment_failed, invoice.payment_succeeded, invoice.upcoming, payment_intent.canceled, payment_intent.payment_failed, payment_intent.succeeded, payment_method.automatically_updated, payment_method.detached, refund.failed, refund.updated, transfer.reversed

The list is StripeWebhookEventMapper::EVENT_TYPES, for a deployment script that creates the endpoint. Mollie needs no selection: it calls the webhook URL the driver sets on each payment.

For the neutral domain events these effects listen on, and how to listen or fake them in your own app, see the Event reference and the Testing guide.


← Back to the documentation index