hotmeteor/receiver
Receiver is a Laravel package for accepting and handling incoming webhooks. Define endpoints, verify provider signatures, and route payloads to events or jobs, making integrations with Stripe, GitHub, and similar services simple and secure.
Install and publish.
composer install
php artisan vendor:publish --tag=receiver-config
Then add a provider entry to config/services.php. v1.0.0's README has an explicit, copy-pasteable block for every built-in provider — start there rather than guessing key names.
// config/services.php
'shopify' => ['secret' => env('SHOPIFY_WEBHOOK_SECRET')],
'twilio' => ['token' => env('TWILIO_AUTH_TOKEN')],
'mailchimp' => ['secret' => env('MAILCHIMP_WEBHOOK_SECRET')],
'sendgrid' => [
'public_key' => env('SENDGRID_VERIFICATION_KEY'), // opt-in ECDSA
],
'paddle' => [
'secret' => env('PADDLE_WEBHOOK_SECRET'),
'previous_secret' => env('PADDLE_WEBHOOK_SECRET_OLD'), // key rotation
],
First use case: log before you build. Point one webhook at a throwaway route and dump the verified event once:
Route::post('/webhooks/{provider}', function (string $provider) {
$receiver = Receiver::driver($provider);
$receiver->validate(request()); // 4xx on bad signature, before handlers run
logger()->info('event', ['event' => $receiver->getEvent()]);
return $receiver->receive(request()); // dispatches handler(s)
});
That single log line tells you the exact event name string and payload shape — which is the only thing you actually need to name your handler class correctly.
Where to look first, in order:
Keep the controller to three lines: resolve the driver, verify, receive. Any normalisation of the payload should happen inside your handler, never before verification — HMAC providers sign the raw bytes.
getEvent() may now return an array of ['event_name' => $eventData] pairs, and the receiver dispatches a separate handler per entry. For batched providers like SendGrid, one HTTP request fans out into many handler invocations. Practically this means:
payload['id'], payload['event_id'], etc.) in a cache lock or a processed_events table.class PaymentAuthorizationCreated implements Handler
{
public function handle(array $payload): void
{
ProcessPaddleEvent::dispatch($payload)->onQueue('webhooks');
}
}
The receiver only has to hand off; anything slower than a few hundred milliseconds belongs in a queued job. Your HTTP response should be a fast 200 so the provider doesn't retry.
Event names are lowercased and converted to StudlyCase before class resolution. PAYMENT.AUTHORIZATION.CREATED → PaymentAuthorizationCreated; push → Push. Because it's deterministic, write one cheap test that asserts class_exists() for every handler you ship:
foreach (['payment.authorization.created', 'subscription.created'] as $event) {
$this->assertTrue(class_exists(Str::studly(str_replace(['.', '-'], '_', $event))));
}
That test catches 90% of "handler never fires" reports before deploy.
php artisan receiver:make MyProvider --provider scaffolds the provider class and a companion ServiceProvider wired for Laravel package auto-discovery, so you can publish it as a standalone Composer package. Inside that package's composer.json, register it under extra.laravel.providers — nothing else.
Postmark now supports auth (HTTP Basic), headers (custom headers), and ips (published Postmark IP ranges), and you can combine them. Day-to-day rule: use auth as the primary, add ips only if you can resolve real client IPs behind your proxy, and use headers when Postmark can't send credentials to your endpoint.
Configure both the current and previous secret, let the provider accept either, rotate in the dashboard, then delete previous_secret after your window. Do not rotate without the overlap.
php://input, will produce valid-looking-but-failing signatures.VerifyCsrfToken (put them in the api group or add them to $except). A 419 is invisible to the provider.config:cache after every services.php edit. A stale cache is the single most common cause of "signature invalid" on a config that is demonstrably correct. Clear it first, debug second.action field (e.g. push, fork) no longer emit push_. If you built workarounds around the old string, remove them and re-check your handler class names against the 1.0.0 naming table. Upgrade notes call it non-breaking, but it does change the resolved name.PAYMENT.AUTHORIZATION.CREATED previously could mis-resolve; anything you named to compensate for that can be deleted.openssl extension.Referer headers, and error trackers. Redact the query string at your reverse proxy and rotate via config, not a code change.ips behind a load balancer. request()->ip() will be your proxy. Either configure TrustProxies correctly or combine ips with auth so a misconfigured proxy doesn't silently block every webhook.config/app.php gives you duplicate registration; pick one.request()->headers->all() plus the raw body before verification. A wrong secret and a wrong driver name produce identical silent 4xx responses — the log distinguishes them in one deploy.composer run lint:check and composer run analyse (PHPStan level 5 + Pint) are the same gates CI applies.How can I help you explore Laravel packages today?