Data

/laravel-agent:webhook:make

Create webhook handlers for external services (Stripe, GitHub, etc.)

Overview

The /webhook:make command generates secure webhook handlers for processing incoming webhooks from external services. It creates controllers with signature verification, event handlers for specific webhook events, middleware for security, and all necessary routing configuration.

Usage

/laravel-agent:webhook:make <ServiceName> [--events=<event1,event2>]

Examples

# Create a Stripe webhook handler with default events
/laravel-agent:webhook:make Stripe

# Create a GitHub webhook handler with specific events
/laravel-agent:webhook:make GitHub --events=push,pull_request

# Create a Paddle webhook handler
/laravel-agent:webhook:make Paddle

# Create a custom webhook handler
/laravel-agent:webhook:make Custom

Supported Services

The command includes pre-configured templates for popular webhook providers:

Service Verification Common Events
Stripe Signature payment_intent.succeeded, customer.subscription.*, invoice.*
Paddle Signature subscription.created, payment.completed
GitHub Signature push, pull_request, issues
GitLab Token push, merge_request
Twilio Signature message.received
SendGrid Signature email.delivered, email.bounced
Custom Configurable User-defined

What Gets Created

A complete webhook handler includes the following components:

Component Location Description
Webhook Controller app/Http/Controllers/Webhooks/ Receives and routes webhook events
Signature Middleware app/Http/Middleware/ Verifies webhook authenticity
Event Handlers app/Webhooks/{Service}/ Processes specific webhook events
Laravel Event app/Events/ For logging and auditing webhooks
Webhook Routes routes/webhooks.php Routes with CSRF exemption

Example Output Structure

For /laravel-agent:webhook:make Stripe:

app/
├── Http/
│   ├── Controllers/Webhooks/
│   │   └── StripeWebhookController.php
│   └── Middleware/
│       └── VerifyStripeSignature.php
├── Webhooks/Stripe/
│   ├── PaymentIntentSucceededHandler.php
│   ├── CustomerSubscriptionCreatedHandler.php
│   ├── CustomerSubscriptionDeletedHandler.php
│   ├── CustomerSubscriptionUpdatedHandler.php
│   ├── InvoicePaidHandler.php
│   └── InvoicePaymentFailedHandler.php
├── Events/
│   └── StripeWebhookReceived.php
└── routes/
    └── webhooks.php

Generated Webhook Controller

Example of a generated Stripe webhook controller:

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Webhooks;

use App\Http\Controllers\Controller;
use App\Events\StripeWebhookReceived;
use App\Webhooks\Stripe\PaymentIntentSucceededHandler;
use App\Webhooks\Stripe\CustomerSubscriptionCreatedHandler;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;

final class StripeWebhookController extends Controller
{
    private array $handlers = [
        'payment_intent.succeeded' => PaymentIntentSucceededHandler::class,
        'customer.subscription.created' => CustomerSubscriptionCreatedHandler::class,
        'customer.subscription.deleted' => CustomerSubscriptionDeletedHandler::class,
        'invoice.paid' => InvoicePaidHandler::class,
    ];

    public function __invoke(Request $request): Response
    {
        $payload = $request->all();
        $eventType = $payload['type'] ?? null;

        Log::info('Stripe webhook received', [
            'type' => $eventType,
            'id' => $payload['id'] ?? null,
        ]);

        // Dispatch event for logging/auditing
        event(new StripeWebhookReceived($eventType, $payload));

        // Handle the event
        if (isset($this->handlers[$eventType])) {
            try {
                $handler = app($this->handlers[$eventType]);
                $handler->handle($payload);
            } catch (\Exception $e) {
                Log::error('Stripe webhook handler failed', [
                    'type' => $eventType,
                    'error' => $e->getMessage(),
                ]);

                // Return 500 for unexpected errors to trigger retry
                if ($this->shouldRetry($e)) {
                    return response('', 500);
                }
            }
        }

        return response('', 200);
    }

    private function shouldRetry(\Exception $e): bool
    {
        // Retry on transient errors (database, network)
        return $e instanceof \Illuminate\Database\QueryException
            || $e instanceof \Illuminate\Http\Client\ConnectionException;
    }
}

Signature Verification Middleware

Example Stripe signature verification middleware:

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Stripe\Webhook;
use Stripe\Exception\SignatureVerificationException;

final class VerifyStripeSignature
{
    public function handle(Request $request, Closure $next): Response
    {
        $signature = $request->header('Stripe-Signature');
        $secret = config('services.stripe.webhook_secret');

        if (!$signature || !$secret) {
            abort(401, 'Missing signature or secret');
        }

        try {
            Webhook::constructEvent(
                $request->getContent(),
                $signature,
                $secret
            );
        } catch (SignatureVerificationException $e) {
            abort(401, 'Invalid signature');
        }

        return $next($request);
    }
}

Event Handler Example

Example handler for processing successful payments:

<?php

declare(strict_types=1);

namespace App\Webhooks\Stripe;

use App\Models\Order;
use App\Models\Payment;
use Illuminate\Support\Facades\Log;

final class PaymentIntentSucceededHandler
{
    public function handle(array $payload): void
    {
        $paymentIntent = $payload['data']['object'];
        $orderId = $paymentIntent['metadata']['order_id'] ?? null;

        if (!$orderId) {
            Log::warning('Payment intent without order_id', [
                'payment_intent_id' => $paymentIntent['id'],
            ]);
            return;
        }

        $order = Order::find($orderId);

        if (!$order) {
            Log::error('Order not found for payment intent', [
                'order_id' => $orderId,
                'payment_intent_id' => $paymentIntent['id'],
            ]);
            return;
        }

        // Record payment
        $payment = Payment::create([
            'order_id' => $order->id,
            'stripe_payment_intent_id' => $paymentIntent['id'],
            'amount_cents' => $paymentIntent['amount'],
            'currency' => $paymentIntent['currency'],
            'status' => 'succeeded',
        ]);

        // Update order status
        $order->update([
            'payment_status' => 'paid',
            'paid_at' => now(),
        ]);

        // Dispatch follow-up events
        event(new \App\Events\OrderPaid($order));

        Log::info('Payment processed successfully', [
            'order_id' => $order->id,
            'payment_id' => $payment->id,
        ]);
    }
}

GitHub Webhook Example

Example GitHub webhook controller with event handling:

<?php

declare(strict_types=1);

namespace App\Http\Controllers\Webhooks;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

final class GitHubWebhookController extends Controller
{
    public function __invoke(Request $request): Response
    {
        $event = $request->header('X-GitHub-Event');
        $payload = $request->all();

        match ($event) {
            'push' => $this->handlePush($payload),
            'pull_request' => $this->handlePullRequest($payload),
            'issues' => $this->handleIssues($payload),
            default => null,
        };

        return response('', 200);
    }

    private function handlePush(array $payload): void
    {
        $ref = $payload['ref'];
        $commits = $payload['commits'] ?? [];
        $repository = $payload['repository']['full_name'];

        // Trigger deployment, notify team, etc.
    }

    private function handlePullRequest(array $payload): void
    {
        $action = $payload['action'];
        $pr = $payload['pull_request'];

        // Handle opened, closed, merged, etc.
    }

    private function handleIssues(array $payload): void
    {
        $action = $payload['action'];
        $issue = $payload['issue'];

        // Handle opened, closed, labeled, etc.
    }
}

GitHub Signature Verification

Example GitHub signature verification middleware:

<?php

declare(strict_types=1);

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

final class VerifyGitHubSignature
{
    public function handle(Request $request, Closure $next): Response
    {
        $signature = $request->header('X-Hub-Signature-256');
        $secret = config('services.github.webhook_secret');

        if (!$signature || !$secret) {
            abort(401);
        }

        $expectedSignature = 'sha256=' . hash_hmac(
            'sha256',
            $request->getContent(),
            $secret
        );

        if (!hash_equals($expectedSignature, $signature)) {
            abort(401, 'Invalid signature');
        }

        return $next($request);
    }
}

Routes Configuration

Webhook routes with signature verification middleware:

// routes/webhooks.php
use App\Http\Controllers\Webhooks\StripeWebhookController;
use App\Http\Controllers\Webhooks\GitHubWebhookController;
use App\Http\Middleware\VerifyStripeSignature;
use App\Http\Middleware\VerifyGitHubSignature;

Route::post('/webhooks/stripe', StripeWebhookController::class)
    ->middleware(VerifyStripeSignature::class)
    ->name('webhooks.stripe');

Route::post('/webhooks/github', GitHubWebhookController::class)
    ->middleware(VerifyGitHubSignature::class)
    ->name('webhooks.github');

CSRF exemption configuration:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'webhooks/*',
    ]);
})

Environment Variables

Required environment configuration for webhook secrets:

# Stripe
STRIPE_WEBHOOK_SECRET=whsec_...

# GitHub
GITHUB_WEBHOOK_SECRET=your-secret

# Paddle
PADDLE_WEBHOOK_SECRET=pdl_...

Options

Interactive prompts guide you through the setup process:

  • Service Selection - Choose from Stripe, Paddle, GitHub, GitLab, Twilio, SendGrid, or Custom
  • Event Selection - Select which webhook events to handle (based on service)
  • Signature Verification - Enable or disable signature verification (recommended: Yes)
  • Event Dispatching - Fire Laravel events for each webhook for logging/auditing

Testing Webhooks

Test your webhook handlers locally:

# Stripe CLI for local testing
stripe listen --forward-to localhost:8000/webhooks/stripe

# Trigger test event
stripe trigger payment_intent.succeeded

# GitHub CLI for testing
gh webhook forward --repo=owner/repo --events=push --url=http://localhost:8000/webhooks/github

Best Practices

  1. Always verify signatures - Protect against unauthorized webhook requests
  2. Use idempotent handlers - Handle duplicate webhook deliveries gracefully
  3. Return 200 quickly - Process webhooks asynchronously in queued jobs for long operations
  4. Log everything - Maintain detailed logs for debugging webhook issues
  5. Handle retries properly - Return 500 for transient errors, 200 for handled errors
  6. Test webhook secrets - Verify environment variables are set correctly before going live
  7. Monitor webhook endpoints - Track success rates and error patterns

Next Steps

After generating webhook handlers:

  1. Add webhook secret to your .env file
  2. Configure the webhook URL in the service's dashboard (e.g., https://yoursite.com/webhooks/stripe)
  3. Test using the service's CLI or testing tools
  4. Monitor application logs for incoming webhook events
  5. Implement additional event handlers as needed

See Also