PHPackages                             vherbaut/laravel-inbound-webhooks - 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. [API Development](/categories/api)
4. /
5. vherbaut/laravel-inbound-webhooks

ActiveLibrary[API Development](/categories/api)

vherbaut/laravel-inbound-webhooks
=================================

A Laravel package to handle inbound webhooks from any provider with signature validation, logging, and replay capabilities.

1.0.1(4mo ago)11MITPHPPHP ^8.1CI passing

Since Dec 28Pushed 4mo agoCompare

[ Source](https://github.com/vherbaut/laravel-inbound-webhooks)[ Packagist](https://packagist.org/packages/vherbaut/laravel-inbound-webhooks)[ RSS](/packages/vherbaut-laravel-inbound-webhooks/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (7)Versions (4)Used By (0)

Laravel Inbound Webhooks
========================

[](#laravel-inbound-webhooks)

[![Tests](https://github.com/vherbaut/laravel-inbound-webhooks/actions/workflows/tests.yml/badge.svg)](https://github.com/vherbaut/laravel-inbound-webhooks/actions/workflows/tests.yml)[![PHPStan](https://camo.githubusercontent.com/0729e562e10fac943b16dbb271b4af26488f779a33fc82cc3eef1e37a432c0b4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c253230352d627269676874677265656e2e737667)](https://phpstan.org/)[![Latest Version on Packagist](https://camo.githubusercontent.com/955efb866fb082141bc05503768cde19edaaebbb05324c2073a4181c4d13e316/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f76686572626175742f6c61726176656c2d696e626f756e642d776562686f6f6b732e737667)](https://packagist.org/packages/vherbaut/laravel-inbound-webhooks)[![Total Downloads](https://camo.githubusercontent.com/c79a571b163a7639d01207010e557563a9fa8c33ce33b53c9e24f89bff622c4a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f76686572626175742f6c61726176656c2d696e626f756e642d776562686f6f6b732e737667)](https://packagist.org/packages/vherbaut/laravel-inbound-webhooks)[![License](https://camo.githubusercontent.com/d398abfed1715424b8fb4351584f84d45c00ff2f0515a40836ff1ef141c57682/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f76686572626175742f6c61726176656c2d696e626f756e642d776562686f6f6b732e737667)](https://packagist.org/packages/vherbaut/laravel-inbound-webhooks)[![PHP Version](https://camo.githubusercontent.com/b79035b952a5d83ac8cd922b354f738fd4dcddc585cdd20031f4a240baef50d9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f76686572626175742f6c61726176656c2d696e626f756e642d776562686f6f6b732e737667)](https://packagist.org/packages/vherbaut/laravel-inbound-webhooks)

A Laravel package to handle inbound webhooks from any provider with signature validation, logging, queue processing, and replay capabilities.

Features
--------

[](#features)

- 🔐 **Signature validation** for Stripe, GitHub, Slack, Twilio, and custom HMAC
- 📦 **Automatic storage** of all incoming webhooks with full payload
- ⚡ **Async processing** via Laravel queues (responds 200 in &lt; 100ms)
- 🔄 **Replay webhooks** for debugging and development
- 🗑️ **Automatic cleanup** with configurable retention
- 📊 **Event mapping** to dispatch custom Laravel events
- 🔌 **Extensible** - add your own drivers easily

Installation
------------

[](#installation)

```
composer require vherbaut/laravel-inbound-webhooks
```

Publish the config and migrations:

```
php artisan vendor:publish --tag=inbound-webhooks-config
php artisan vendor:publish --tag=inbound-webhooks-migrations
php artisan migrate
```

Quick Start
-----------

[](#quick-start)

### 1. Configure your provider

[](#1-configure-your-provider)

Add your webhook secret to `.env`:

```
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
```

### 2. Register your webhook URL

[](#2-register-your-webhook-url)

Point your provider's webhook settings to:

```
https://your-app.com/webhooks/stripe

```

### 3. Listen for events

[](#3-listen-for-events)

In your `EventServiceProvider` or a listener:

```
use Vherbaut\InboundWebhooks\Events\WebhookReceived;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        WebhookReceived::class => [
            HandleStripeWebhook::class,
        ],
    ];
}
```

```
// app/Listeners/HandleStripeWebhook.php

class HandleStripeWebhook
{
    public function handle(WebhookReceived $event): void
    {
        if ($event->provider() !== 'stripe') {
            return;
        }

        match ($event->eventType()) {
            'payment_intent.succeeded' => $this->handlePaymentSucceeded($event),
            'customer.subscription.deleted' => $this->handleSubscriptionCanceled($event),
            default => null,
        };
    }

    protected function handlePaymentSucceeded(WebhookReceived $event): void
    {
        $paymentIntentId = $event->get('data.object.id');
        $amount = $event->get('data.object.amount');

        // Your logic here...
    }
}
```

Configuration
-------------

[](#configuration)

### Providers

[](#providers)

Configure each provider in `config/inbound-webhooks.php`:

```
'providers' => [
    'stripe' => [
        'driver' => 'stripe',
        'secret' => env('STRIPE_WEBHOOK_SECRET'),
        'tolerance' => 300, // Timestamp tolerance in seconds
    ],

    'github' => [
        'driver' => 'github',
        'secret' => env('GITHUB_WEBHOOK_SECRET'),
    ],

    'slack' => [
        'driver' => 'slack',
        'signing_secret' => env('SLACK_SIGNING_SECRET'),
    ],

    'twilio' => [
        'driver' => 'twilio',
        'auth_token' => env('TWILIO_AUTH_TOKEN'),
    ],

    // Custom provider using generic HMAC
    'acme' => [
        'driver' => 'hmac',
        'secret' => env('ACME_WEBHOOK_SECRET'),
        'algorithm' => 'sha256',
        'header' => 'X-Acme-Signature',
        'prefix' => 'sha256=', // Optional prefix
        'event_key' => 'event_type',
        'id_key' => 'webhook_id',
    ],
],
```

### Event Mapping

[](#event-mapping)

Map specific webhook events to custom Laravel event classes:

```
'events' => [
    'stripe.payment_intent.succeeded' => \App\Events\PaymentReceived::class,
    'stripe.customer.subscription.deleted' => \App\Events\SubscriptionCanceled::class,
    'github.push' => \App\Events\GitHubPush::class,
],
```

Your custom event receives the webhook model:

```
// app/Events/PaymentReceived.php

class PaymentReceived
{
    public function __construct(
        public InboundWebhook $webhook
    ) {}
}
```

Events
------

[](#events)

The package dispatches three events during the webhook lifecycle:

### WebhookReceived

[](#webhookreceived)

Dispatched when a webhook is received and ready for processing. This is the primary event for handling webhooks.

```
use Vherbaut\InboundWebhooks\Events\WebhookReceived;

class HandleStripeWebhook
{
    public function handle(WebhookReceived $event): void
    {
        // Access webhook data via helper methods
        $provider = $event->provider();           // "stripe"
        $eventType = $event->eventType();         // "payment_intent.succeeded"
        $payload = $event->payload();             // Full payload array
        $value = $event->get('data.object.id');   // Dot notation access
    }
}
```

### WebhookProcessed

[](#webhookprocessed)

Dispatched after a webhook has been successfully processed. Useful for metrics or cleanup.

```
use Vherbaut\InboundWebhooks\Events\WebhookProcessed;

class UpdateMetrics
{
    public function handle(WebhookProcessed $event): void
    {
        Metrics::increment("webhooks.{$event->webhook->provider}.success");
    }
}
```

### WebhookFailed

[](#webhookfailed)

Dispatched when webhook processing fails. Use for error notifications or logging.

```
use Vherbaut\InboundWebhooks\Events\WebhookFailed;

class NotifyOnFailure
{
    public function handle(WebhookFailed $event): void
    {
        Log::error('Webhook failed', [
            'provider' => $event->webhook->provider,
            'event_type' => $event->webhook->event_type,
            'error' => $event->exception->getMessage(),
        ]);

        // Send notification to Slack, email, etc.
    }
}
```

### Registering Listeners

[](#registering-listeners)

Register your listeners in `EventServiceProvider`:

```
use Vherbaut\InboundWebhooks\Events\WebhookReceived;
use Vherbaut\InboundWebhooks\Events\WebhookProcessed;
use Vherbaut\InboundWebhooks\Events\WebhookFailed;

protected $listen = [
    WebhookReceived::class => [
        HandleStripeWebhook::class,
        HandleGitHubWebhook::class,
    ],
    WebhookProcessed::class => [
        UpdateWebhookMetrics::class,
    ],
    WebhookFailed::class => [
        NotifyOnWebhookFailure::class,
        RetryFailedWebhook::class,
    ],
];
```

### Queue Configuration

[](#queue-configuration)

```
'queue' => [
    'connection' => env('INBOUND_WEBHOOKS_QUEUE_CONNECTION', 'default'),
    'queue' => env('INBOUND_WEBHOOKS_QUEUE', 'webhooks'),
],
```

### Storage

[](#storage)

```
'storage' => [
    'store_payload' => true,    // Store full payload (recommended for replay)
    'retention_days' => 30,     // Auto-prune after 30 days (null = forever)
],
```

Console Commands
----------------

[](#console-commands)

### Replay a webhook

[](#replay-a-webhook)

```
# By UUID
php artisan webhooks:replay 550e8400-e29b-41d4-a716-446655440000

# Process synchronously (useful for debugging)
php artisan webhooks:replay 550e8400-e29b-41d4-a716-446655440000 --sync

# Force replay even if already processed
php artisan webhooks:replay 550e8400-e29b-41d4-a716-446655440000 --force
```

### Prune old webhooks

[](#prune-old-webhooks)

```
# Use configured retention days
php artisan webhooks:prune

# Custom retention
php artisan webhooks:prune --days=7

# Only prune failed webhooks
php artisan webhooks:prune --status=failed

# Only prune specific provider
php artisan webhooks:prune --provider=stripe

# Dry run (see what would be deleted)
php artisan webhooks:prune --dry-run
```

Schedule automatic pruning in `app/Console/Kernel.php`:

```
$schedule->command('webhooks:prune')->daily();
```

Custom Drivers
--------------

[](#custom-drivers)

Create custom drivers to integrate with any webhook provider not covered by the built-in drivers.

### Registering a Custom Driver

[](#registering-a-custom-driver)

Register your driver in the `boot()` method of a service provider:

```
// app/Providers/AppServiceProvider.php

use Vherbaut\InboundWebhooks\Facades\InboundWebhooks;
use App\Webhooks\Drivers\PayPalDriver;

public function boot(): void
{
    InboundWebhooks::extend('paypal', function (array $config) {
        return new PayPalDriver($config);
    });
}
```

### Configuration

[](#configuration-1)

Add your provider configuration in `config/inbound-webhooks.php`:

```
'providers' => [
    'paypal' => [
        'driver' => 'paypal',                              // Must match the name used in extend()
        'webhook_id' => env('PAYPAL_WEBHOOK_ID'),          // Custom config keys
        'client_id' => env('PAYPAL_CLIENT_ID'),
        'client_secret' => env('PAYPAL_CLIENT_SECRET'),
        'sandbox' => env('PAYPAL_SANDBOX', true),
    ],
],
```

### Creating the Driver Class

[](#creating-the-driver-class)

Extend `AbstractDriver` and implement the required methods from `DriverInterface`:

```
// app/Webhooks/Drivers/PayPalDriver.php

namespace App\Webhooks\Drivers;

use Illuminate\Http\Request;
use Vherbaut\InboundWebhooks\Drivers\AbstractDriver;
use Vherbaut\InboundWebhooks\Exceptions\InvalidSignatureException;

class PayPalDriver extends AbstractDriver
{
    /**
     * Validate the webhook signature.
     *
     * @throws InvalidSignatureException
     */
    public function validateSignature(Request $request): void
    {
        $transmissionId = $request->header('Paypal-Transmission-Id');
        $timestamp = $request->header('Paypal-Transmission-Time');
        $signature = $request->header('Paypal-Transmission-Sig');
        $certUrl = $request->header('Paypal-Cert-Url');

        if (! $transmissionId || ! $signature) {
            throw new InvalidSignatureException('Missing PayPal signature headers');
        }

        // Build the expected signature string
        $webhookId = $this->config['webhook_id'];
        $payload = $request->getContent();
        $crc32 = crc32($payload);

        $expectedSignature = "{$transmissionId}|{$timestamp}|{$webhookId}|{$crc32}";

        // Verify with PayPal certificate (simplified example)
        if (! $this->verifyWithCertificate($expectedSignature, $signature, $certUrl)) {
            throw new InvalidSignatureException('Invalid PayPal webhook signature');
        }
    }

    /**
     * Extract the event type from the webhook payload.
     */
    public function getEventType(Request $request): ?string
    {
        return $request->input('event_type');
    }

    /**
     * Extract the external ID (PayPal's webhook event ID).
     */
    public function getExternalId(Request $request): ?string
    {
        return $request->input('id');
    }

    /**
     * Define provider-specific headers to store for auditing.
     *
     * @return array
     */
    protected function getRelevantHeaders(): array
    {
        return [
            'Content-Type',
            'Paypal-Transmission-Id',
            'Paypal-Transmission-Time',
            'Paypal-Transmission-Sig',
            'Paypal-Cert-Url',
        ];
    }

    private function verifyWithCertificate(
        string $data,
        string $signature,
        ?string $certUrl
    ): bool {
        // Your verification logic here
        return true;
    }
}
```

### Using AbstractDriver Helpers

[](#using-abstractdriver-helpers)

The `AbstractDriver` class provides utility methods for common signature validation patterns:

```
class AcmeDriver extends AbstractDriver
{
    public function validateSignature(Request $request): void
    {
        $signature = $request->header('X-Acme-Signature');
        $payload = $request->getContent();
        $secret = $this->config['secret'];

        if (! $signature) {
            throw new InvalidSignatureException('Missing signature header');
        }

        // Use built-in HMAC computation
        $expected = $this->computeHmac($payload, $secret, 'sha256');

        // Use timing-safe comparison to prevent timing attacks
        if (! $this->compareSignatures($expected, $signature)) {
            throw new InvalidSignatureException('Invalid signature');
        }
    }

    // ...
}
```

**Available helper methods:**

MethodDescription`computeHmac($payload, $secret, $algorithm)`Compute HMAC signature (default: sha256)`compareSignatures($expected, $actual)`Timing-safe string comparison`getRelevantHeaders()`Override to define storable headers`getPayload($request)`Override to customize payload extraction`getStorableHeaders($request)`Filters headers based on `getRelevantHeaders()`### Custom Payload Extraction

[](#custom-payload-extraction)

Override `getPayload()` if your provider sends data in a non-standard format:

```
public function getPayload(Request $request): array
{
    // Handle form-encoded webhooks (e.g., Twilio)
    if ($request->isJson()) {
        return $request->json()->all();
    }

    return $request->all();
}
```

### Full Driver Interface Reference

[](#full-driver-interface-reference)

Your driver must implement all methods from `DriverInterface`:

```
interface DriverInterface
{
    /**
     * Validate the webhook signature.
     *
     * @throws InvalidSignatureException
     */
    public function validateSignature(Request $request): void;

    /**
     * Extract the event type from the webhook payload.
     */
    public function getEventType(Request $request): ?string;

    /**
     * Extract the external ID (provider's webhook/event ID).
     */
    public function getExternalId(Request $request): ?string;

    /**
     * Get the parsed payload from the request.
     */
    public function getPayload(Request $request): array;

    /**
     * Get headers that should be stored with the webhook.
     */
    public function getStorableHeaders(Request $request): array;
}
```

### Testing Custom Drivers

[](#testing-custom-drivers)

```
use App\Webhooks\Drivers\PayPalDriver;
use Illuminate\Http\Request;
use Vherbaut\InboundWebhooks\Exceptions\InvalidSignatureException;

it('validates paypal webhook signature', function () {
    $driver = new PayPalDriver([
        'webhook_id' => 'WH-123',
        'client_id' => 'test',
        'client_secret' => 'secret',
    ]);

    $request = Request::create('/webhooks/paypal', 'POST', [], [], [], [
        'HTTP_PAYPAL_TRANSMISSION_ID' => 'abc123',
        'HTTP_PAYPAL_TRANSMISSION_SIG' => 'valid-signature',
        'HTTP_PAYPAL_TRANSMISSION_TIME' => '2024-01-15T10:00:00Z',
    ], json_encode(['event_type' => 'PAYMENT.CAPTURE.COMPLETED']));

    // Should not throw
    $driver->validateSignature($request);
});

it('rejects invalid signature', function () {
    $driver = new PayPalDriver(['webhook_id' => 'WH-123']);

    $request = Request::create('/webhooks/paypal', 'POST');

    $driver->validateSignature($request);
})->throws(InvalidSignatureException::class);
```

Querying Webhooks
-----------------

[](#querying-webhooks)

```
use Vherbaut\InboundWebhooks\Models\InboundWebhook;

// Get all failed Stripe webhooks
$failed = InboundWebhook::provider('stripe')
    ->failed()
    ->latest()
    ->get();

// Get recent payment webhooks
$payments = InboundWebhook::provider('stripe')
    ->eventType('payment_intent.succeeded')
    ->where('created_at', '>', now()->subDay())
    ->get();

// Retry all failed webhooks
InboundWebhook::failed()->each(function ($webhook) {
    $webhook->resetForRetry();
    ProcessWebhook::dispatch($webhook);
});
```

Testing
-------

[](#testing)

In your tests, you can simulate incoming webhooks:

```
use Vherbaut\InboundWebhooks\Models\InboundWebhook;

// Create a webhook directly for testing
$webhook = InboundWebhook::create([
    'provider' => 'stripe',
    'event_type' => 'payment_intent.succeeded',
    'payload' => [
        'type' => 'payment_intent.succeeded',
        'data' => [
            'object' => [
                'id' => 'pi_123',
                'amount' => 1000,
            ],
        ],
    ],
]);

// Process it
ProcessWebhook::dispatchSync($webhook);
```

License
-------

[](#license)

MIT

###  Health Score

34

—

LowBetter than 77% of packages

Maintenance75

Regular maintenance activity

Popularity3

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 75% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

135d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4bf0f9ef2e911f1dd3e00d6cb3d6487f8db22b94f94eba6e0c4ef41b87d9061e?d=identicon)[vherbaut](/maintainers/vherbaut)

---

Top Contributors

[![vincent-akawam](https://avatars.githubusercontent.com/u/166985545?v=4)](https://github.com/vincent-akawam "vincent-akawam (3 commits)")[![vherbaut](https://avatars.githubusercontent.com/u/8347782?v=4)](https://github.com/vherbaut "vherbaut (1 commits)")

---

Tags

apilaravelstripegithubwebhooksinbound

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/vherbaut-laravel-inbound-webhooks/health.svg)

```
[![Health](https://phpackages.com/badges/vherbaut-laravel-inbound-webhooks/health.svg)](https://phpackages.com/packages/vherbaut-laravel-inbound-webhooks)
```

###  Alternatives

[essa/api-tool-kit

set of tools to build an api with laravel

52680.5k](/packages/essa-api-tool-kit)[phpsa/laravel-postman

Export laravel API routes to postman

1014.7k](/packages/phpsa-laravel-postman)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
