Webhooks
Vatly sends webhooks to notify your application of events like subscription starts, cancellations, and payment updates. Vatly Laravel handles webhook verification, storage, and event dispatching automatically.
Endpoint
The package registers a webhook endpoint at:
POST /webhooks/vatly
Configure this URL in your Vatly dashboard. Make sure to set your VATLY_WEBHOOK_SECRET in .env.
CSRF protection
Exclude the webhook route from CSRF verification. In Laravel 11+, this is typically done in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhooks/vatly',
]);
})
Events
When a webhook is received, the driver's LaravelEventDispatcher forwards the typed domain events straight onto Laravel's event bus, so you listen for the DTO classes directly. The webhook event DTOs live in vatly-api-php under the Vatly\API\Webhooks\Events\ namespace (so a payload change is a single api-php release); the exceptions are SubscriptionWasCreatedFromWebhook and OrderWasCreatedFromWebhook, internal fluent signals under Vatly\Fluent\Events\:
Event (Vatly\API\Webhooks\Events\…) | Dispatched when |
|---|---|
SubscriptionStarted | A subscription.started webhook is received |
SubscriptionBillingUpdated | A subscription.billing_updated webhook is received — the stored mandate is refreshed |
SubscriptionResumed | A subscription.resumed webhook is received — the stored end date is cleared |
SubscriptionCanceledImmediately | A subscription.canceled_immediately webhook is received |
SubscriptionCanceledWithGracePeriod | A subscription.canceled_with_grace_period webhook is received |
SubscriptionCancellationGracePeriodCompleted | A subscription.cancellation_grace_period_completed webhook is received — the grace period stamped by the cancellation has now elapsed (carries customerId, subscriptionId, endsAt) |
OrderPaid | An order.paid webhook is received (enriched with the full tax breakdown) |
OrderCanceled | An order.canceled webhook is received — the local order status is mirrored to canceled |
OrderPaymentFailed | An order.payment_failed webhook is received — typically the start of dunning (enriched with the full tax breakdown) |
OrderChargebackReceived / OrderChargebackReversed | An order.chargeback_received / order.chargeback_reversed webhook is received — enriched with customerId, dispute status, totals and taxSummary; persisted to vatly_chargebacks |
RefundCompleted / RefundFailed / RefundCanceled | A refund.completed / refund.failed / refund.canceled webhook is received — each carries the full taxSummary; persisted to vatly_refunds |
CheckoutPaid | A checkout.paid webhook is received — the hosted checkout was paid (fires before order.paid's enrichment GET; carries checkoutId, nullable customerId / orderId, status, metadata) |
CheckoutFailed | A checkout.failed webhook is received — the hosted checkout's payment failed (route into a retry flow) |
CheckoutCanceled | A checkout.canceled webhook is received — the customer abandoned the hosted checkout (cart-abandonment hook) |
CheckoutExpired | A checkout.expired webhook is received — the hosted checkout session timed out without completion |
WebhookSetupReceived | A webhook.setup verification call is received — Vatly confirms a newly registered (or re-pointed) endpoint is reachable. There's no resource to enrich and no local row to touch; just acknowledge with a 2xx. Carries only the webhook envelope (eventName / object) |
UnsupportedWebhookReceived | A webhook arrives that has no typed mapping (carries the raw eventName / object) |
SubscriptionWasCreatedFromWebhook (in Vatly\Fluent\Events\) | A new local Subscription row was just created from a subscription.started webhook (application-level event; carries the stored $subscription) |
OrderWasCreatedFromWebhook (in Vatly\Fluent\Events\) | The order analogue of SubscriptionWasCreatedFromWebhook: a new local Order row was just created from an order.paid webhook (fires once on a brand-new order; carries the stored $order). A clean hook for receipts / fulfillment |
Money fields. On the order and refund events (OrderPaid, OrderPaymentFailed, RefundCompleted / RefundFailed / RefundCanceled), total and subtotal are non-null Vatly\API\Types\Money value objects (a decimal-string value plus a currency); read the currency via $event->total->currency and minor units via $event->total->toCents(). These events no longer carry a standalone currency field. The chargeback events (OrderChargebackReceived / OrderChargebackReversed) carry nullable ?Money total / subtotal and keep their standalone currency field.
Exactly one of the webhook events above is dispatched per incoming webhook (UnsupportedWebhookReceived is the fallback for unmapped events). SubscriptionWasCreatedFromWebhook and OrderWasCreatedFromWebhook fire additionally, from the subscription- and order-sync reactions, only when a brand-new local row is created.
Built-in reactions
Before the event is dispatched, the package keeps your local tables in sync automatically via fluent's standard webhook reactions. These are wired by WebhookProcessorFactory inside the Vatly composition root — no registration needed on your side. They live under Vatly\Fluent\Webhooks\Reactions\:
SyncSubscriptionOnStarted-- OnSubscriptionStarted, creates (or updates) the localSubscriptionrow, then dispatchesSubscriptionWasCreatedFromWebhookfor newly-created rows.CancelSubscriptionOnCanceled-- OnSubscriptionCanceledImmediately/SubscriptionCanceledWithGracePeriod, sets the local subscription'sends_at.StoreOrderOnPaid-- OnOrderPaid, stores (or updates) the localOrderrow, then dispatchesOrderWasCreatedFromWebhookfor newly-created rows.StoreOrderOnPaymentFailed-- OnOrderPaymentFailed, stores (or updates) the localOrderrow, mirroring the upstream order status verbatim.EndSubscriptionOnGracePeriodCompleted-- OnSubscriptionCancellationGracePeriodCompleted, stamps the actualends_atonto the local subscription. Usually an idempotent re-write of whatCancelSubscriptionOnCanceledalready stored, but it self-heals a missed/out-of-ordersubscription.canceled_with_grace_periodwebhook (which would otherwise leaveends_atnull and the subscription looking active forever) and corrects any drift between the scheduled and actual end.
The checkout events (CheckoutPaid / CheckoutFailed / CheckoutCanceled / CheckoutExpired) ship no built-in reaction — they touch no local table. The checkout payloads carry the full Checkout resource (so they're dispatched without an enrichment GET), and there is no local checkout entity to keep in sync. Listen for them directly to drive receipt/retry/cart-abandonment UI or to flip your own application-level state. SubscriptionCancellationGracePeriodCompleted is likewise dispatched (on top of the reaction above) for consumers that want to flip their own application-level "fully ended" status.
Custom listeners
Listen for the events in your EventServiceProvider or using the Event facade:
use Illuminate\Support\Facades\Event;
use Vatly\API\Webhooks\Events\SubscriptionStarted;
Event::listen(SubscriptionStarted::class, function (SubscriptionStarted $event) {
// $event->customerId
// $event->subscriptionId
// $event->planId
// $event->type
// $event->name
// $event->quantity
// Send welcome email, provision features, etc.
});
Order events (OrderPaid / OrderPaymentFailed) carry the full, API-enriched order — including the tax breakdown — so you can materialize an invoice without a follow-up API call:
use Illuminate\Support\Facades\Event;
use Vatly\API\Webhooks\Events\OrderPaid;
Event::listen(OrderPaid::class, function (OrderPaid $event) {
// $event->orderId
// $event->customerId
// $event->status
// $event->total // Vatly\API\Types\Money
// $event->subtotal // Vatly\API\Types\Money
// $event->total->currency // e.g. "EUR"
// $event->total->value // decimal string, e.g. "99.00"
// $event->total->toCents() // minor units (cents), e.g. 9900
// $event->taxSummary
// $event->invoiceNumber
// $event->paymentMethod
// $event->metadata
});
Webhook call storage
Every webhook is recorded in the vatly_webhook_calls table with:
vatly_id-- The webhook event ID (unique; use this as your dedup key)resource-- The wrapper resource type (alwayswebhook_event)event_name-- The webhook event type (e.g.,subscription.started)entity_type-- The resource type the event relates to (e.g.,subscription)entity_id-- The Vatly resource ID the event relates totestmode-- Whether the event was raised against a testmode entityvatly_created_at-- When the webhook event was created at Vatlyvatly_customer_id-- The associated customer ID, when presentobject-- The full resource payload at the time of the event (JSON)