Webhooks¶
Webhook routes are registered automatically:
Queueing: Set PAYMENT_WEBHOOK_QUEUE_EVENTS=true to return 202 immediately and process the payload on the queue. The raw body is preserved so signature verification still works in the worker.
Idempotency: Set PAYMENT_WEBHOOK_IDEMPOTENCY_ENABLED=true to ignore duplicate deliveries with the same body (cache key per gateway + payload hash). Failed processing releases the lock so the provider can retry.
Signing secrets: With PAYMENT_WEBHOOK_VERIFY_SIGNATURE and PAYMENT_WEBHOOK_REQUIRE_SIGNING_SECRET both true (defaults), gateways that use HMAC (or Stripe’s signing secret) return 400 if the secret is missing, so you never verify with an empty key. M-Pesa, MTN MoMo, Remita, and PayPal skip this check (no shared HMAC secret in the same sense).
Unexpected errors: On HTTP 500 from the webhook controller, only the error message is logged by default; stack traces are included when APP_DEBUG is true. Set PAYMENT_WEBHOOK_LOG_UNEXPECTED_TRACE=true only if you explicitly want traces in logs (e.g. staging).
Rate limiting: Publish config and add the package middleware to payment-gateways.webhooks.middleware:
// config/payment-gateways.php
'webhooks' => [
// ...
'throttle' => [
'enabled' => env('PAYMENT_WEBHOOK_THROTTLE_ENABLED', false),
// ...
],
'middleware' => [
\Alphabalex\PaymentMadeEasy\Http\Middleware\ThrottlePaymentWebhooks::class,
],
],
payment:webhook-replay bypasses signature verification by design; keep it console-only in production.
Configure each gateway's dashboard to point to:
https://yoursite.com/webhooks/payment-gateways/paystack
https://yoursite.com/webhooks/payment-gateways/flutterwave
https://yoursite.com/webhooks/payment-gateways/stripe
https://yoursite.com/webhooks/payment-gateways/seerbit
https://yoursite.com/webhooks/payment-gateways/monnify
https://yoursite.com/webhooks/payment-gateways/squad
https://yoursite.com/webhooks/payment-gateways/remita
https://yoursite.com/webhooks/payment-gateways/budpay
https://yoursite.com/webhooks/payment-gateways/interswitch
https://yoursite.com/webhooks/payment-gateways/paypal
https://yoursite.com/webhooks/payment-gateways/mpesa
https://yoursite.com/webhooks/payment-gateways/mtnmomo
https://yoursite.com/webhooks/payment-gateways/razorpay
https://yoursite.com/webhooks/payment-gateways/paddle
Available Events¶
| Event Class | Fired When |
|---|---|
PaymentSuccessful |
A payment completes successfully |
PaymentFailed |
A payment fails |
PaymentPending |
A payment is pending |
RefundProcessed |
A refund is processed |
SubscriptionCreated |
A subscription is activated |
SubscriptionCancelled |
A subscription is cancelled |
SubscriptionRenewed |
A subscription renews / invoice paid |
TransferSuccessful |
A transfer/payout succeeds |
TransferFailed |
A transfer/payout fails |
DisputeCreated |
A dispute is raised |
ChargebackCreated |
A chargeback is raised |
Listening to Events¶
Register listeners in app/Providers/EventServiceProvider.php (or Laravel 11+ AppServiceProvider / bootstrap/app.php event discovery as you prefer):
use Alphabalex\PaymentMadeEasy\Events\PaymentSuccessful;
use Alphabalex\PaymentMadeEasy\Events\PaymentFailed;
use Alphabalex\PaymentMadeEasy\Events\PaymentPending;
use Alphabalex\PaymentMadeEasy\Events\RefundProcessed;
use Alphabalex\PaymentMadeEasy\Events\SubscriptionCreated;
use Alphabalex\PaymentMadeEasy\Events\SubscriptionCancelled;
use Alphabalex\PaymentMadeEasy\Events\SubscriptionRenewed;
use Alphabalex\PaymentMadeEasy\Events\TransferSuccessful;
use Alphabalex\PaymentMadeEasy\Events\TransferFailed;
use Alphabalex\PaymentMadeEasy\Events\DisputeCreated;
use Alphabalex\PaymentMadeEasy\Events\ChargebackCreated;
protected $listen = [
PaymentSuccessful::class => [App\Listeners\HandleSuccessfulPayment::class],
PaymentFailed::class => [App\Listeners\HandleFailedPayment::class],
PaymentPending::class => [App\Listeners\HandlePendingPayment::class],
RefundProcessed::class => [App\Listeners\HandleRefundProcessed::class],
SubscriptionCreated::class => [App\Listeners\HandleSubscriptionCreated::class],
SubscriptionCancelled::class => [App\Listeners\HandleSubscriptionCancelled::class],
SubscriptionRenewed::class => [App\Listeners\HandleSubscriptionRenewed::class],
TransferSuccessful::class => [App\Listeners\HandleTransferSuccessful::class],
TransferFailed::class => [App\Listeners\HandleTransferFailed::class],
DisputeCreated::class => [App\Listeners\HandleDisputeCreated::class],
ChargebackCreated::class => [App\Listeners\HandleChargebackCreated::class],
];
// app/Listeners/HandleSuccessfulPayment.php
class HandleSuccessfulPayment
{
public function handle(PaymentSuccessful $event): void
{
$gateway = $event->webhookEvent->getGateway(); // e.g. 'paystack'
$eventType = $event->webhookEvent->getEventType(); // e.g. 'charge.success'
$data = $event->paymentData;
// Normalized keys commonly include: reference, amount, currency, status,
// customer_email, transaction_date, metadata
// update order, send receipt, etc.
}
}
Subscription and transfer listeners receive typed payloads: $event->subscriptionData, $event->invoiceData (renewals), $event->transferData, $event->reason (cancellations / failed transfers), $event->disputeData, $event->chargebackData.
Manual processing with WebhookManager¶
For a custom route (e.g. non-standard path), resolve WebhookManager and pass the gateway slug plus the request. Catch WebhookException for invalid signatures or bad payloads.
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Alphabalex\PaymentMadeEasy\WebhookManager;
use Alphabalex\PaymentMadeEasy\Exceptions\WebhookException;
class CustomWebhookController extends Controller
{
public function handle(Request $request, string $gateway, WebhookManager $webhookManager)
{
try {
$webhookManager->handle($gateway, $request);
return response()->json(['status' => 'success']);
} catch (WebhookException $e) {
return response()->json(['error' => $e->getMessage()], 400);
}
}
}
Optional .env toggles (see the Installation env block for the full list):
PAYMENT_WEBHOOKS_ENABLED=true
PAYMENT_WEBHOOK_VERIFY_SIGNATURE=true
PAYMENT_WEBHOOK_LOG_EVENTS=true
PAYMENT_WEBHOOK_QUEUE_EVENTS=false
PAYSTACK_WEBHOOK_SECRET=your_paystack_webhook_secret
FLUTTERWAVE_WEBHOOK_SECRET=your_flutterwave_webhook_secret
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
SEERBIT_WEBHOOK_SECRET=your_seerbit_webhook_secret