PHPackages                             proxynth/larawebhook - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. proxynth/larawebhook

ActiveLibrary[Payment Processing](/categories/payments)

proxynth/larawebhook
====================

LaraWebhook is an open-source Laravel package for securely and reliably managing incoming webhooks. It validates signatures, handles retries, and logs errors transparently. Ideal for Stripe, GitHub, Slack, and more!

v1.20.0(1mo ago)10MITPHPPHP ^8.3CI passing

Since Jan 26Pushed 1w agoCompare

[ Source](https://github.com/proxynth/LaraWebhook)[ Packagist](https://packagist.org/packages/proxynth/larawebhook)[ Docs](https://github.com/proxynth/larawebhook)[ GitHub Sponsors](https://github.com/:proxynth)[ RSS](/packages/proxynth-larawebhook/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (10)Dependencies (39)Versions (45)Used By (0)

LaraWebhook 🚀
=============

[](#larawebhook-)

[![Latest Version](https://camo.githubusercontent.com/e272eae1a7b7c583d60591636fd01faa612e6dc5ddfda579fb9a8403510570d2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f70726f78796e74682f6c617261776562686f6f6b2e737667)](https://packagist.org/packages/proxynth/larawebhook)[![Tests](https://github.com/proxynth/larawebhook/actions/workflows/tests.yml/badge.svg)](https://github.com/proxynth/larawebhook/actions)[![Codecov](https://camo.githubusercontent.com/bbc4b73c41ec45eab7852b527cb041c5f27f13c2f60636a82ae853f6058a00e4/68747470733a2f2f636f6465636f762e696f2f6769746875622f70726f78796e74682f4c617261576562686f6f6b2f67726170682f62616467652e7376673f746f6b656e3d34574746544138484452)](https://codecov.io/github/proxynth/LaraWebhook)[![PHPStan](https://github.com/proxynth/larawebhook/actions/workflows/phpstan.yml/badge.svg)](https://github.com/proxynth/larawebhook/actions)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)

**LaraWebhook** is an open-source Laravel package for handling incoming webhooks in a **secure, reliable, and simple** way. Validate signatures, manage retries, log events, and integrate popular services (Stripe, GitHub, Slack, etc.) in minutes.

---

Project status
--------------

[](#project-status)

LaraWebhook is currently an experimental side project and MVP-like Laravel package.

It explores webhook reliability patterns such as signature validation, idempotency, logging, replay, provider-specific parsing, and privacy-conscious payload handling.

The package is usable for experimentation, internal tools, prototypes, and non-critical workloads. However, it should not be considered production-hardened by default for sensitive or business-critical webhook flows.

Before using LaraWebhook in production, you should carefully review and configure:

- dashboard and API access control;
- payload storage strategy;
- sensitive data redaction;
- log retention policy;
- replay permissions;
- provider secrets and signature validation;
- idempotency behavior.

The long-term direction is to evolve LaraWebhook into a safer, stricter, privacy-first webhook toolkit for Laravel, potentially as part of the broader Sovra ecosystem.

### Long-term direction

[](#long-term-direction)

LaraWebhook may eventually become the Laravel integration layer for **Sovra**, a broader exploration around sovereign, privacy-first and self-hostable webhook infrastructure.

For now, LaraWebhook remains an experimental Laravel package focused on webhook validation, logging, replay and safer payload handling.
---------------------------------------------------------------------------------------------------------------------------------------

[](#for-now-larawebhook-remains-an-experimental-laravel-package-focused-on-webhook-validation-logging-replay-and-safer-payload-handling)

Production readiness
--------------------

[](#production-readiness)

LaraWebhook can be used as a foundation for production webhook handling, but it should not be deployed blindly with default settings for sensitive or business-critical flows.

Before using LaraWebhook in production, review the following checklist. You can also read `PRODUCTION_CHECKLIST.md`

### Access control

[](#access-control)

Make sure the dashboard and API routes are not publicly exposed.

Recommended actions:

- disable the dashboard if you do not need it;
- protect dashboard routes with authentication middleware;
- protect API routes with token-based authentication such as Laravel Sanctum;
- restrict replay endpoints to trusted users only.

### Payload storage

[](#payload-storage)

Webhook payloads may contain sensitive or personal data such as emails, names, addresses, tokens, payment references, customer identifiers, or internal metadata.

Recommended actions:

- avoid storing full payloads unless strictly required;
- prefer redacted payloads or metadata-only storage;
- document why payloads are stored;
- avoid exposing raw payloads in logs, dashboards, notifications or error messages.

### Sensitive data redaction

[](#sensitive-data-redaction)

If payloads are stored, sensitive fields should be masked before persistence.

Recommended actions:

- redact fields such as `email`, `phone`, `address`, `token`, `secret`, `authorization`, `client_secret`, `password`;
- review provider-specific payloads;
- test redaction rules with real-world-like examples;
- never rely on manual cleanup after storage.

### Retention policy

[](#retention-policy)

Webhook logs should not be kept forever by default.

Recommended actions:

- configure a retention period;
- prune old webhook logs regularly;
- keep shorter retention for full or redacted payloads;
- document your retention policy according to your application requirements.

### Replay permissions

[](#replay-permissions)

Replay is useful for recovery and debugging, but it can trigger business actions again.

Recommended actions:

- restrict replay access;
- log replay attempts;
- avoid replaying non-idempotent handlers;
- make sure your application handlers are safe to execute more than once;
- consider disabling replay for sources where payloads are not stored.

### Provider secrets

[](#provider-secrets)

Webhook validation depends on provider secrets.

Recommended actions:

- store secrets in environment variables or a secret manager;
- never commit secrets to the repository;
- rotate secrets when needed;
- use different secrets per environment;
- verify that invalid signatures are rejected.

### Idempotency

[](#idempotency)

Webhook providers may send the same event more than once.

Recommended actions:

- configure idempotency behavior per provider;
- use stable provider event IDs when available;
- fallback to payload hashes only when appropriate;
- make downstream handlers idempotent too.

### Monitoring and failure handling

[](#monitoring-and-failure-handling)

Validation and delivery failures should be visible.

Recommended actions:

- monitor failed validations;
- monitor failed processing attempts;
- alert on repeated failures;
- distinguish invalid signatures from downstream processing errors.

### Compliance and responsibility

[](#compliance-and-responsibility)

LaraWebhook can help implement safer webhook handling practices, but compliance depends on your own application, configuration, data, infrastructure, contracts and operational procedures.

For more details, see [`SECURITY_AND_PRIVACY.md`](SECURITY_AND_PRIVACY.md).

### Dashboard

[](#dashboard)

The dashboard is disabled by default.

To enable it:

```
LARAWEBHOOK_DASHBOARD_ENABLED=true
```

Before enabling the dashboard in production, make sure dashboard routes are protected with authentication middleware.

### Dashboard middleware

[](#dashboard-middleware)

The dashboard is disabled by default.

When enabling it, you should protect it with authentication middleware:

```
'dashboard' => [
    'enabled' => env('LARAWEBHOOK_DASHBOARD_ENABLED', false),
    'path' => env('LARAWEBHOOK_DASHBOARD_PATH', 'larawebhook/dashboard'),
    'middleware' => ['web', 'auth'],
],
```

You may also use authorization gates or custom middleware:

```
'middleware' => ['web', 'auth', 'can:viewLaraWebhookDashboard'],
```

### API routes

[](#api-routes)

The LaraWebhook API is disabled by default.

To enable it:

```
LARAWEBHOOK_API_ENABLED=true
```

When enabling API routes in production, protect them with authentication middleware such as Laravel Sanctum or a custom token-based middleware.

The replay endpoint should be restricted to trusted users only, as replaying webhook events may trigger business actions again.

### API middleware

[](#api-middleware)

The LaraWebhook API is disabled by default.

When enabling API routes in production, you should protect them with authentication middleware:

```
'api' => [
    'enabled' => env('LARAWEBHOOK_API_ENABLED', false),
    'path' => env('LARAWEBHOOK_API_PATH', 'api/larawebhook'),
    'middleware' => ['api', 'auth:sanctum'],
],
```

The replay endpoint should be restricted to trusted users only, as replaying webhook events may trigger business actions again.

You may also use authorization gates or custom middleware:

```
'middleware' => ['api', 'auth:sanctum', 'can:manageLaraWebhook'],
```

### Payload storage

[](#payload-storage-1)

Webhook payloads may contain sensitive or personal data.

LaraWebhook supports three payload storage modes:

```
'payload_storage' => [
    'mode' => env('LARAWEBHOOK_PAYLOAD_STORAGE_MODE', 'redacted'),
],
```

Supported modes:

ModeDescription`none`Do not store the webhook payload. Only metadata should be persisted.`redacted`Store a sanitized version of the payload. Sensitive fields are masked before persistence.`full`Store the full payload. This is useful for debugging and replay, but should be explicitly enabled only when required.The `full` mode may store personal or sensitive data depending on the provider payload. Use it carefully and configure a retention policy.

> Redacted payload storage is being hardened progressively. Until redaction rules are configured, avoid assuming that all provider-specific sensitive fields are covered. In the current implementation, `redacted` mode avoids storing raw payloads until the redaction engine is fully available.

### Retention policy

[](#retention-policy-1)

Webhook logs should not be kept forever by default.
LaraWebhook provides a retention configuration that will be used by the prune command:

```
'retention' => [
    'enabled' => env('LARAWEBHOOK_RETENTION_ENABLED', true),
    'days' => (int) env('LARAWEBHOOK_RETENTION_DAYS', 30),
],
```

By default, webhook logs become eligible for pruning after 30 days.
You should adjust this value according to your debugging needs, payload storage mode, legal requirements and internal data retention policies.

The actual pruning command is handled separately by:

```
php artisan larawebhook:prune
```

A prune command will use this configuration to determine which logs are eligible for deletion.

### Pruning old webhook logs

[](#pruning-old-webhook-logs)

LaraWebhook provides a prune command to delete old webhook logs according to your retention policy.

```
php artisan larawebhook:prune
```

By default, the command uses:

```
'retention' => [
    'enabled' => true,
    'days' => 30,
],
```

You can override the retention period at runtime:

```
php artisan larawebhook:prune --older-than=7d
```

Supported duration units:

UnitMeaningddayshhoursmminutesTo preview how many logs would be deleted without deleting them:

```
php artisan larawebhook:prune --older-than=30d --dry-run
```

### Scheduler exemple

[](#scheduler-exemple)

You can schedule pruning in your Laravel application:

```
use Illuminate\Support\Facades\Schedule;

Schedule::command('larawebhook:prune')->daily();
```

---

### Sensitive data redaction

[](#sensitive-data-redaction-1)

LaraWebhook includes a payload redaction service that can mask sensitive fields before payloads are stored.
Default sensitive fields include:

```
[
    'email',
    'phone',
    'address',
    'token',
    'secret',
    'authorization',
    'client_secret',
    'password',
    'api_key',
    'access_token',
    'refresh_token',
]
```

When redaction is applied, matching fields are replaced with:

```
[REDACTED]

```

Matching is case-insensitive and recursive.

---

✨ Features
----------

[](#-features)

- **Signature Validation**: Verify webhook authenticity (Stripe, GitHub, Slack, Shopify)
- **Automatic Idempotency**: Duplicate webhooks are automatically rejected with `200 OK`
- **Async Retry Management**: Queue failed webhooks for background retry (returns 202 Accepted)
- **Detailed Logging**: Database logs + Laravel logs (`Log::info/error`) for debugging
- **Failure Notifications**: Get alerted via Email and Slack when webhooks fail repeatedly
- **Interactive Dashboard**: Modern UI with Alpine.js and Tailwind CSS for log management
- **REST API**: Programmatic access to webhook logs with filtering and pagination
- **Replay Webhooks**: Re-process failed webhooks from dashboard or API
- **Fluent Facade API**: Simple and expressive API via `Larawebhook` facade
- **Type-Safe Services**: `WebhookService` enum for IDE autocompletion and type safety
- **Easy Integration**: Minimal configuration, compatible with Laravel 9+
- **Extensible Architecture**: Strategy Pattern for parsers and validators - add new services in minutes

---

📦 Installation
--------------

[](#-installation)

1. Install the package via Composer:

    ```
    composer require proxynth/larawebhook
    ```
2. Publish the configuration:

    ```
    php artisan vendor:publish --provider="Proxynth\LaraWebhook\LaraWebhookServiceProvider"
    ```
3. Configure your signature keys in `config/larawebhook.php`:

    ```
    'stripe' => [
         'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
         'tolerance' => 300, // Tolerance in seconds
    ],
    ```

---

🛠 Usage
-------

[](#-usage)

### Using the Middleware (Recommended)

[](#using-the-middleware-recommended)

The easiest way to validate webhooks is using the `validate-webhook` middleware:

```
// routes/web.php
Route::post('/stripe-webhook', function () {
    // Webhook is automatically validated and logged
    // Process your webhook here
    $payload = json_decode(request()->getContent(), true);

    // Handle the event
    event(new \App\Events\StripeWebhookReceived($payload));

    return response()->json(['status' => 'success']);
})->middleware('validate-webhook:stripe');

Route::post('/github-webhook', function () {
    // Webhook is automatically validated and logged
    $payload = json_decode(request()->getContent(), true);

    // Handle the event
    event(new \App\Events\GithubWebhookReceived($payload));

    return response()->json(['status' => 'success']);
})->middleware('validate-webhook:github');
```

**What the middleware does:**

- ✅ Validates the webhook signature
- ✅ Automatically logs the event to the database
- ✅ Rejects duplicate webhooks (returns `200 OK` with `already_processed`)
- ✅ Returns 403 for invalid signatures
- ✅ Returns 400 for missing headers or malformed payloads

### Manual Validation (Advanced)

[](#manual-validation-advanced)

For more control, you can manually validate webhooks:

```
// app/Http/Controllers/WebhookController.php
use Proxynth\Larawebhook\Facades\Larawebhook;
use Proxynth\Larawebhook\Ingestion\Domain\ValueObjects\Signature;
use Illuminate\Http\Request;

public function handleWebhook(Request $request)
{
    $payload = $request->getContent();
    $signature = Signature::fromString(
        $request->header('Stripe-Signature')
    );

    $secret = config('larawebhook.services.stripe.webhook_secret');

    try {
        // Validate and log in one call
        $log = Larawebhook::validateAndLog(
            $payload,
            $signature,
            'stripe',
            'payment_intent.succeeded'
        );

        // Process the event
        event(new \App\Events\StripeWebhookReceived(json_decode($payload, true)));

        return response()->json(['status' => 'success']);
    } catch (\Exception $e) {
        return response($e->getMessage(), 403);
    }
}
```

---

🎯 Facade &amp; Enum API
-----------------------

[](#-facade--enum-api)

LaraWebhook provides a powerful Facade and an Enum for type-safe service handling.

### Using the Facade

[](#using-the-facade)

The `Larawebhook` facade provides a fluent API for all webhook operations:

```
use Proxynth\Larawebhook\Shared\Infrastructure\Laravel\Facades\Larawebhook;

// Validate a webhook
Larawebhook::validate($payload, $signature, 'stripe');

// Validate and log
$log = Larawebhook::validateAndLog($payload, $signature, 'github', 'push');

// Log webhooks manually
Larawebhook::logSuccess('stripe', 'payment.succeeded', $payload);
Larawebhook::logFailure('stripe', 'payment.failed', $payload, 'Card declined');

// Query logs
$allLogs = Larawebhook::logs();
$stripeLogs = Larawebhook::logsForService('stripe');
$failedLogs = Larawebhook::failedLogs();
$successLogs = Larawebhook::successfulLogs();

// Notifications
Larawebhook::sendNotificationIfNeeded('stripe', 'payment.failed');
Larawebhook::notificationsEnabled(); // true/false
Larawebhook::getNotificationChannels(); // ['mail', 'slack']

// Configuration helpers
Larawebhook::getSecret('stripe'); // Returns webhook secret
Larawebhook::isServiceSupported('stripe'); // true
Larawebhook::supportedServices(); // ['stripe', 'github']
```

### WebhookService Enum

[](#webhookservice-enum)

The `WebhookService` enum centralizes all service-related configuration:

```
use Proxynth\Larawebhook\Enums\WebhookService;

// Available services
WebhookService::Stripe; // 'stripe'
WebhookService::Github; // 'github'

// Get signature header for a service
WebhookService::Stripe->signatureHeader(); // 'Stripe-Signature'
WebhookService::Github->signatureHeader(); // 'X-Hub-Signature-256'

// Get secret from config
WebhookService::Stripe->secret(); // Returns configured secret

// Get the payload parser (for extracting event types and metadata)
WebhookService::Stripe->parser(); // StripePayloadParser
WebhookService::Github->parser(); // GithubPayloadParser

// Get the signature validator (for verifying webhook authenticity)
WebhookService::Stripe->signatureValidator(); // StripeSignatureValidator
WebhookService::Github->signatureValidator(); // GithubSignatureValidator

// Check if a service is supported
WebhookService::isSupported('stripe'); // true
WebhookService::isSupported('unknown'); // false

// Convert from string
$service = WebhookService::tryFromString('stripe'); // WebhookService::Stripe
$service = WebhookService::fromString('stripe'); // WebhookService::Stripe (throws on invalid)

// Get all values (useful for validation rules)
WebhookService::values(); // ['stripe', 'github']
WebhookService::validationRule(); // ['stripe', 'github']
```

### Using Enum with Facade

[](#using-enum-with-facade)

All facade methods accept both strings and the enum:

```
use Proxynth\Larawebhook\Shared\Infrastructure\Laravel\Facades\Larawebhook;
use Proxynth\Larawebhook\Enums\WebhookService;

// Both are equivalent
Larawebhook::validate($payload, $signature, 'stripe');
Larawebhook::validate($payload, $signature, WebhookService::Stripe);

// Type-safe service handling
$service = WebhookService::Stripe;
$log = Larawebhook::validateAndLog($payload, $signature, $service, 'payment.succeeded');
```

### Benefits of Using the Enum

[](#benefits-of-using-the-enum)

- **Type Safety**: IDE autocompletion and static analysis support
- **Centralized Configuration**: All service-related config in one place
- **DRY Principle**: No more duplicated service strings across the codebase
- **Easy Extension**: Add a new service by adding a case to the enum

---

🏗️ Extensible Architecture
--------------------------

[](#️-extensible-architecture)

LaraWebhook uses the **Strategy Pattern** for maximum extensibility. Each webhook service has its own:

- **PayloadParser**: Extracts event types and metadata from the webhook payload
- **SignatureValidator**: Validates the webhook signature according to the provider's format

### Architecture Overview

[](#architecture-overview)

```
src/
├── Contracts/
│   ├── PayloadParserInterface.php        # Strategy interface for parsing
│   └── SignatureValidatorInterface.php   # Strategy interface for validation
├── Parsers/
│   ├── StripePayloadParser.php           # Stripe payload parsing
│   └── GithubPayloadParser.php           # GitHub payload parsing
├── Validators/
│   ├── StripeSignatureValidator.php      # Stripe signature validation
│   └── GithubSignatureValidator.php      # GitHub signature validation
└── Enums/
    └── WebhookService.php                # Central delegation point

```

### Adding a New Service (Example: PayPal)

[](#adding-a-new-service-example-paypal)

Adding a new webhook service requires just 4 steps:

**Step 1: Create the Payload Parser**

```
// src/Parsers/PaypalPayloadParser.php
namespace Proxynth\Larawebhook\Parsers;

use Proxynth\Larawebhook\Contracts\PayloadParserInterface;

class PaypalPayloadParser implements PayloadParserInterface
{
    public function extractEventType(array $data): string
    {
        return $data['event_type'] ?? 'unknown';
    }

    public function extractMetadata(array $data): array
    {
        return [
            'event_id' => $data['id'] ?? null,
            'resource_type' => $data['resource_type'] ?? null,
            'summary' => $data['summary'] ?? null,
        ];
    }

    public function serviceName(): string
    {
        return 'paypal';
    }
}
```

**Step 2: Create the Signature Validator**

```
// src/Validators/PaypalSignatureValidator.php
namespace Proxynth\Larawebhook\Validators;

use Proxynth\Larawebhook\Contracts\SignatureValidatorInterface;
use Proxynth\Larawebhook\Exceptions\InvalidSignatureException;

class PaypalSignatureValidator implements SignatureValidatorInterface
{
    public function validate(string $payload, string $signature, string $secret, int $tolerance = 300): bool
    {
        // PayPal uses base64-encoded HMAC-SHA256
        $expected = base64_encode(hash_hmac('sha256', $payload, $secret, true));

        if (! hash_equals($expected, $signature)) {
            throw new InvalidSignatureException('Invalid PayPal webhook signature.');
        }

        return true;
    }

    public function serviceName(): string
    {
        return 'paypal';
    }
}
```

**Step 3: Register in the Enum**

```
// src/Enums/WebhookService.php
enum WebhookService: string
{
    case Stripe = 'stripe';
    case Github = 'github';
    case Paypal = 'paypal';  // Add the new case

    public function parser(): PayloadParserInterface
    {
        return match ($this) {
            self::Stripe => new StripePayloadParser,
            self::Github => new GithubPayloadParser,
            self::Paypal => new PaypalPayloadParser,  // Add mapping
        };
    }

    public function signatureValidator(): SignatureValidatorInterface
    {
        return match ($this) {
            self::Stripe => new StripeSignatureValidator,
            self::Github => new GithubSignatureValidator,
            self::Paypal => new PaypalSignatureValidator,  // Add mapping
        };
    }

    public function signatureHeader(): string
    {
        return match ($this) {
            self::Stripe => 'Stripe-Signature',
            self::Github => 'X-Hub-Signature-256',
            self::Paypal => 'PAYPAL-TRANSMISSION-SIG',  // Add header
        };
    }
}
```

**Step 4: Add Configuration**

```
// config/larawebhook.php
'services' => [
    'paypal' => [
        'webhook_secret' => env('PAYPAL_WEBHOOK_SECRET'),
        'tolerance' => 300,
    ],
],
```

That's it! Your new service is now fully integrated:

```
// Use with middleware
Route::post('/paypal-webhook', [PaypalController::class, 'handle'])
    ->middleware('validate-webhook:paypal');

// Or with the facade
Larawebhook::validate($payload, $signature, WebhookService::Paypal);
```

### Using Parsers Directly

[](#using-parsers-directly)

You can access parsers directly for custom payload processing:

```
use Proxynth\Larawebhook\Enums\WebhookService;

$payload = json_decode($request->getContent(), true);

// Extract event type
$eventType = WebhookService::Stripe->parser()->extractEventType($payload);
// Returns: 'payment_intent.succeeded'

// Extract metadata
$metadata = WebhookService::Github->parser()->extractMetadata($payload);
// Returns: ['delivery_id' => '...', 'action' => 'opened', 'sender' => 'octocat', ...]
```

### Using Validators Directly

[](#using-validators-directly)

For advanced use cases, you can use validators directly:

```
use Proxynth\Larawebhook\Enums\WebhookService;

$isValid = WebhookService::Stripe->signatureValidator()->validate(
    payload: $rawPayload,
    signature: $signatureHeader,
    secret: config('larawebhook.services.stripe.webhook_secret'),
    tolerance: 300
);
```

---

🔌 Service Integration Examples
------------------------------

[](#-service-integration-examples)

Complete integration guides with real-world examples for popular webhook providers.

### 🔵 Stripe Integration

[](#-stripe-integration)

#### 1. Configuration

[](#1-configuration)

Add your Stripe webhook secret to `.env`:

```
STRIPE_WEBHOOK_SECRET=whsec_your_stripe_webhook_secret_here
```

Then configure the service in `config/larawebhook.php`:

```
'services' => [
    'stripe' => [
        'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
        'tolerance' => 300, // 5 minutes tolerance for timestamp validation
    ],
],
```

#### 2. Create Route and Controller

[](#2-create-route-and-controller)

**Define the webhook route** in `routes/web.php`:

```
use App\Http\Controllers\StripeWebhookController;

Route::post('/stripe-webhook', [StripeWebhookController::class, 'handle'])
    ->middleware('validate-webhook:stripe');
```

**Create the controller** at `app/Http/Controllers/StripeWebhookController.php`:

```
