hotmeteor / receiver
A drop-in webhook handling library for Laravel
Requires
- php: ^8.2
- illuminate/support: >= 10.0
Requires (Dev)
- laravel/pint: ^1.29
- nunomaduro/collision: ^7.0|^8.0
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.0|^11.0|^12.0
- stripe/stripe-php: ^13.0
Suggests
- stripe/stripe-php: Required to use the Stripe provider
README
Receiver
Receiver is a drop-in webhook handling library for Laravel.
Receiver gives you a consistent, expressive way to receive, verify, and handle incoming webhooks in your Laravel app. Point a route at a controller, call three methods, and you're done.
Out of the box, Receiver supports:
| Provider | Driver |
|---|---|
| GitHub | github |
| HubSpot | hubspot |
| Mailchimp Marketing | mailchimp |
| Paddle Billing | paddle |
| Postmark | postmark |
| SendGrid Events | sendgrid |
| Shopify | shopify |
| Slack Events API | slack |
| Stripe | stripe |
| Twilio | twilio |
Any other webhook source can be added with a custom provider.
Table of Contents
- Installation
- Configuration
- Receiving Webhooks
- Handling Webhooks
- Extending Receiver
- Share Your Receivers!
- Credits
- License
Installation
Requires PHP ^8.2 and Laravel 10+.
composer require hotmeteor/receiver
Note: The Stripe provider requires
stripe/stripe-php:composer require stripe/stripe-php
Configuration
Each provider reads its secret from config/services.php. Add an entry for each source you intend to receive from.
Most providers use the same shape:
'github' => ['webhook_secret' => env('GITHUB_WEBHOOK_SECRET')], 'hubspot' => ['webhook_secret' => env('HUBSPOT_WEBHOOK_SECRET')], 'paddle' => ['webhook_secret' => env('PADDLE_WEBHOOK_SECRET')], 'shopify' => ['webhook_secret' => env('SHOPIFY_WEBHOOK_SECRET')], 'slack' => ['webhook_secret' => env('SLACK_WEBHOOK_SECRET')], 'stripe' => ['webhook_secret' => env('STRIPE_WEBHOOK_SECRET')], 'twilio' => ['webhook_secret' => env('TWILIO_AUTH_TOKEN')],
Mailchimp — Mailchimp Marketing webhooks are verified via a secret you embed in your webhook URL (?secret=...). Configure the same value here so Receiver can compare it:
'mailchimp' => ['webhook_secret' => env('MAILCHIMP_WEBHOOK_SECRET')],
SendGrid — Signature verification is opt-in. Set webhook_secret to the PEM-format public key found in the SendGrid dashboard under Settings → Mail Settings → Event Webhook. Leave it empty to accept all requests without verification.
'sendgrid' => ['webhook_secret' => env('SENDGRID_WEBHOOK_PUBLIC_KEY', '')],
Postmark — Postmark supports several verification strategies. Configure which ones to use under the webhook key:
'postmark' => [ 'token' => env('POSTMARK_TOKEN'), 'webhook' => [ // One or more of: 'auth', 'headers', 'ips' 'verification_types' => ['headers', 'ips'], // Header name => expected value pairs (used with 'headers') 'headers' => [ 'X-Custom-Header' => env('POSTMARK_WEBHOOK_HEADER'), ], // Allowed source IPs (used with 'ips') // https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks 'ips' => [ '3.134.147.250', '50.31.156.6', '50.31.156.77', '18.217.206.57', ], ], ],
Postmark verification_type |
Description |
|---|---|
auth |
HTTP Basic Auth via Auth::onceBasic() |
headers |
Validates that specific request headers match expected values |
ips |
Validates that the request originates from an allowed IP |
If verification_types is empty or not set, all Postmark requests are accepted without verification.
Receiving Webhooks
Single provider
Create a controller and route for each webhook source, then call driver(), receive(), and ok():
<?php namespace App\Http\Controllers\Webhooks; use Illuminate\Http\Request; use Receiver\Facades\Receiver; class StripeWebhookController extends Controller { public function store(Request $request) { return Receiver::driver('stripe') ->receive($request) ->ok(); } }
driver()— selects the provider and reads its configreceive()— verifies the signature and maps the eventok()— dispatches matched handlers and returns a200response
Multiple providers
If you'd rather handle all webhooks through a single controller, use a {provider} route parameter:
// routes/web.php Route::post('/webhooks/{provider}', [WebhookController::class, 'store']) ->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class);
<?php namespace App\Http\Controllers\Webhooks; use Illuminate\Http\Request; use Receiver\Facades\Receiver; class WebhookController extends Controller { public function store(Request $request, string $provider) { return Receiver::driver($provider) ->receive($request) ->ok(); } }
Or use the included ReceivesWebhooks trait, which provides this exact store() method for you:
<?php namespace App\Http\Controllers\Webhooks; use Receiver\ReceivesWebhooks; class WebhookController extends Controller { use ReceivesWebhooks; }
Fallbacks
Receiver silently ignores events it has no handler for. If you'd like to do something with unhandled events, add a fallback() callback before ok():
use Receiver\Providers\Webhook; return Receiver::driver($provider) ->receive($request) ->fallback(function (Webhook $webhook) { Log::info('Unhandled webhook', ['event' => $webhook->getEvent()]); }) ->ok();
Handling Webhooks
Once a webhook is received, Receiver looks for a handler class that matches the event and dispatches it. Handlers live in App\Http\Handlers\{Driver}\ by default. If no matching handler is found the webhook is silently ignored and a 200 is returned.
Handler naming
The handler class name is derived from the event name — all non-alphanumeric characters are treated as word separators, then converted to StudlyCase:
| Event name | Handler class |
|---|---|
customer.created |
CustomerCreated |
subscription_activated |
SubscriptionActivated |
orders_created |
OrdersCreated |
invoice.payment_failed |
InvoicePaymentFailed |
For example, Stripe's customer.created event dispatches App\Http\Handlers\Stripe\CustomerCreated.
Each handler receives the $event name and the $data array, and must use the Dispatchable trait:
<?php namespace App\Http\Handlers\Stripe; use Illuminate\Foundation\Bus\Dispatchable; class CustomerCreated { use Dispatchable; public function __construct( public string $event, public array $data, ) {} public function handle(): void { // Your code here } }
Queueing handlers
Because Receiver calls dispatch() on each handler, making a handler queued is as simple as implementing ShouldQueue:
<?php namespace App\Http\Handlers\Stripe; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; class CustomerCreated implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( public string $event, public array $data, ) {} public function handle(): void { // Your code here } }
See the Laravel queue documentation for full details.
Extending Receiver
A provider is a PHP class that tells Receiver how to extract the event name, the payload data, and optionally how to verify the request's authenticity.
Generating a provider
The quickest way to scaffold a new provider is with the receiver:make Artisan command:
# Basic provider php artisan receiver:make Mailgun # With signature verification scaffolded php artisan receiver:make Mailgun --verified
The generated class is placed in App\Http\Receivers. Once created, register the driver in your AppServiceProvider:
public function boot(): void { app('receiver')->extend('mailgun', function () { return new \App\Http\Receivers\MailgunProvider( config('services.mailgun.webhook_secret') ); }); }
Defining getEvent() and getData()
Implement getEvent() to return the event name. Optionally implement getData() to return the event payload — by default it returns $request->all().
<?php namespace App\Http\Receivers; use Illuminate\Http\Request; use Receiver\Providers\AbstractProvider; class MailgunProvider extends AbstractProvider { public function getEvent(Request $request): string|array { return $request->input('event-data.event'); } public function getData(Request $request): array { return $request->input('event-data', []); } }
Securing webhooks
Implement a verify() method that returns true if the request is authentic, or false to reject it with a 401 response:
public function verify(Request $request): bool { $signature = $request->header('X-Mailgun-Signature'); $expected = hash_hmac('sha256', $request->getContent(), $this->secret); return hash_equals($expected, (string) $signature); }
The signing secret from config/services.{driver}.webhook_secret is available as $this->secret.
Handshakes
Some services send a verification request when a webhook URL is first registered. Implement handshake() to respond to it:
public function handshake(Request $request): array { return ['challenge' => $request->input('challenge')]; }
When handshake() returns a non-empty array, Receiver responds immediately with that payload and skips normal event handling. When it returns an empty array, Receiver processes the request normally.
Multiple events per request
Some services batch multiple events into a single request. Return an ['event_name' => $eventData] array from getEvent() and Receiver will dispatch a separate handler for each entry:
public function getEvent(Request $request): string|array { $events = []; foreach (json_decode($request->getContent(), true) as $event) { $type = $event['type'] ?? null; if ($type && ! isset($events[$type])) { $events[$type] = $event; } } return $events; }
Creating a community provider
If you're building a reusable provider package to share, add the --provider flag to also generate a companion ServiceProvider that registers the driver automatically:
php artisan receiver:make Mailgun --provider php artisan receiver:make Mailgun --verified --provider
This generates:
app/Http/Receivers/MailgunProvider.php— your provider classapp/Providers/MailgunReceiverServiceProvider.php— auto-registers the driver viaReceiver::extend()
To support Laravel package auto-discovery, add the service provider to your package's composer.json:
{
"extra": {
"laravel": {
"providers": [
"YourVendor\\YourPackage\\MailgunReceiverServiceProvider"
]
}
}
}
Users who install your package will have the driver available immediately, with no manual registration required.
Share Your Receivers!
Built a provider for a service not listed above? Share it with the community in the Receivers Discussion topic!
Credits
Made with contributors-img.
License
The MIT License (MIT). Please see License File for more information.
