Skip to content

Webhooks

Webhook routes are registered automatically:

POST /webhooks/payment-gateways/{gateway}

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