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!
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: *
- spatie/laravel-package-tools: ^1.12
- symfony/mailer: ^7.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.27
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
README
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.
β¨ 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
Larawebhookfacade - Type-Safe Services:
WebhookServiceenum 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
-
Install the package via Composer:
composer require proxynth/larawebhook
-
Publish the configuration:
php artisan vendor:publish --provider="Proxynth\LaraWebhook\LaraWebhookServiceProvider" -
Configure your signature keys in
config/larawebhook.php:'stripe' => [ 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), 'tolerance' => 300, // Tolerance in seconds ],
π Usage
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 OKwithalready_processed) - β Returns 403 for invalid signatures
- β Returns 400 for missing headers or malformed payloads
Manual Validation (Advanced)
For more control, you can manually validate webhooks:
// app/Http/Controllers/WebhookController.php use Proxynth\Larawebhook\Services\WebhookValidator; use Illuminate\Http\Request; public function handleWebhook(Request $request) { $payload = $request->getContent(); $signature = $request->header('Stripe-Signature'); $secret = config('larawebhook.services.stripe.webhook_secret'); $validator = new WebhookValidator($secret); try { // Validate and log in one call $log = $validator->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 & Enum API
LaraWebhook provides a powerful Facade and an Enum for type-safe service handling.
Using the Facade
The Larawebhook facade provides a fluent API for all webhook operations:
use Proxynth\Larawebhook\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
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
All facade methods accept both strings and the enum:
use Proxynth\Larawebhook\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
- 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
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
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 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
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
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
Complete integration guides with real-world examples for popular webhook providers.
π΅ Stripe Integration
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
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:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Log; class StripeWebhookController extends Controller { public function handle(Request $request): JsonResponse { // Webhook is already validated by the middleware $payload = json_decode($request->getContent(), true); $event = $payload['type'] ?? 'unknown'; // Route to specific event handlers match ($event) { 'payment_intent.succeeded' => $this->handlePaymentIntentSucceeded($payload), 'payment_intent.payment_failed' => $this->handlePaymentIntentFailed($payload), 'charge.succeeded' => $this->handleChargeSucceeded($payload), 'charge.failed' => $this->handleChargeFailed($payload), 'customer.subscription.created' => $this->handleSubscriptionCreated($payload), 'customer.subscription.updated' => $this->handleSubscriptionUpdated($payload), 'customer.subscription.deleted' => $this->handleSubscriptionDeleted($payload), 'invoice.paid' => $this->handleInvoicePaid($payload), 'invoice.payment_failed' => $this->handleInvoicePaymentFailed($payload), default => $this->handleUnknownEvent($event, $payload), }; return response()->json(['status' => 'success']); } private function handlePaymentIntentSucceeded(array $payload): void { $paymentIntent = $payload['data']['object']; Log::info('Stripe: Payment intent succeeded', [ 'payment_intent_id' => $paymentIntent['id'], 'amount' => $paymentIntent['amount'], 'currency' => $paymentIntent['currency'], 'customer' => $paymentIntent['customer'], ]); // Example: Update order status in your database // Order::where('stripe_payment_intent_id', $paymentIntent['id']) // ->update(['status' => 'paid']); } private function handlePaymentIntentFailed(array $payload): void { $paymentIntent = $payload['data']['object']; Log::error('Stripe: Payment intent failed', [ 'payment_intent_id' => $paymentIntent['id'], 'last_payment_error' => $paymentIntent['last_payment_error'], ]); // Example: Notify customer of payment failure // $order = Order::where('stripe_payment_intent_id', $paymentIntent['id'])->first(); // Mail::to($order->customer->email)->send(new PaymentFailedMail($order)); } private function handleChargeSucceeded(array $payload): void { $charge = $payload['data']['object']; Log::info('Stripe: Charge succeeded', [ 'charge_id' => $charge['id'], 'amount' => $charge['amount'], ]); } private function handleChargeFailed(array $payload): void { $charge = $payload['data']['object']; Log::error('Stripe: Charge failed', [ 'charge_id' => $charge['id'], 'failure_message' => $charge['failure_message'], ]); } private function handleSubscriptionCreated(array $payload): void { $subscription = $payload['data']['object']; Log::info('Stripe: Subscription created', [ 'subscription_id' => $subscription['id'], 'customer' => $subscription['customer'], 'status' => $subscription['status'], ]); // Example: Grant access to premium features // User::where('stripe_customer_id', $subscription['customer']) // ->update(['subscription_status' => 'active']); } private function handleSubscriptionUpdated(array $payload): void { $subscription = $payload['data']['object']; Log::info('Stripe: Subscription updated', [ 'subscription_id' => $subscription['id'], 'status' => $subscription['status'], ]); } private function handleSubscriptionDeleted(array $payload): void { $subscription = $payload['data']['object']; Log::info('Stripe: Subscription deleted', [ 'subscription_id' => $subscription['id'], ]); // Example: Revoke access to premium features // User::where('stripe_customer_id', $subscription['customer']) // ->update(['subscription_status' => 'cancelled']); } private function handleInvoicePaid(array $payload): void { $invoice = $payload['data']['object']; Log::info('Stripe: Invoice paid', [ 'invoice_id' => $invoice['id'], 'amount_paid' => $invoice['amount_paid'], ]); } private function handleInvoicePaymentFailed(array $payload): void { $invoice = $payload['data']['object']; Log::error('Stripe: Invoice payment failed', [ 'invoice_id' => $invoice['id'], 'attempt_count' => $invoice['attempt_count'], ]); } private function handleUnknownEvent(string $event, array $payload): void { Log::warning('Stripe: Unknown event type received', [ 'event_type' => $event, ]); } }
3. Webhook Flow Diagram
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
β β β β β β
β Stripe Server ββββββββββΆβ LaraWebhook ββββββββββΆβ Your Application β
β β POST β - Validates β Valid β - Process event β
β (Webhook) β β signature β β - Update database β
β β β - Logs event β β - Send emails β
βββββββββββββββββββ β - Returns response β β β
ββββββββββββββββββββββββ βββββββββββββββββββββββ
β
β Invalid signature
βΌ
ββββββββββββββββββββββββ
β Returns 403 β
β Forbidden β
ββββββββββββββββββββββββ
4. Example Log Entry
Successful webhook processing creates a log entry:
{
"id": 1,
"service": "stripe",
"event": "payment_intent.succeeded",
"status": "success",
"payload": {
"id": "evt_1234567890",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_1234567890",
"amount": 5000,
"currency": "usd",
"customer": "cus_1234567890",
"status": "succeeded"
}
}
},
"attempt": 0,
"error_message": null,
"created_at": "2024-01-15 10:30:00"
}
5. Configure Webhook in Stripe Dashboard
- Go to Stripe Dashboard
- Click Add endpoint
- Enter your webhook URL:
https://your-domain.com/stripe-webhook - Select events to listen for (or select "receive all events")
- Copy the Signing secret (starts with
whsec_) and add it to your.envfile
6. Testing & Debugging
View webhook logs:
php artisan tinker >>> \Proxynth\LaraWebhook\Models\WebhookLog::where('service', 'stripe')->latest()->first();
Test with Stripe CLI:
# Install Stripe CLI brew install stripe/stripe-cli/stripe # Forward webhooks to your local environment stripe listen --forward-to http://localhost:8000/stripe-webhook # Trigger a test webhook stripe trigger payment_intent.succeeded
β« GitHub Integration
1. Configuration
Add your GitHub webhook secret to .env:
GITHUB_WEBHOOK_SECRET=your_github_webhook_secret_here
Then configure the service in config/larawebhook.php:
'services' => [ 'github' => [ 'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'), 'tolerance' => 300, ], ],
2. Create Route and Controller
Define the webhook route in routes/web.php:
use App\Http\Controllers\GitHubWebhookController; Route::post('/github-webhook', [GitHubWebhookController::class, 'handle']) ->middleware('validate-webhook:github');
Create the controller at app/Http/Controllers/GitHubWebhookController.php:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Log; class GitHubWebhookController extends Controller { public function handle(Request $request): JsonResponse { // Webhook is already validated by the middleware $payload = json_decode($request->getContent(), true); $event = $request->header('X-GitHub-Event'); // Route to specific event handlers match ($event) { 'push' => $this->handlePush($payload), 'pull_request' => $this->handlePullRequest($payload), 'pull_request_review' => $this->handlePullRequestReview($payload), 'issues' => $this->handleIssues($payload), 'issue_comment' => $this->handleIssueComment($payload), 'release' => $this->handleRelease($payload), 'workflow_run' => $this->handleWorkflowRun($payload), 'deployment' => $this->handleDeployment($payload), 'star' => $this->handleStar($payload), default => $this->handleUnknownEvent($event, $payload), }; return response()->json(['status' => 'success']); } private function handlePush(array $payload): void { $repository = $payload['repository']['full_name']; $branch = str_replace('refs/heads/', '', $payload['ref']); $commits = count($payload['commits']); $pusher = $payload['pusher']['name']; Log::info('GitHub: Push event received', [ 'repository' => $repository, 'branch' => $branch, 'commits' => $commits, 'pusher' => $pusher, ]); // Example: Trigger deployment for main branch // if ($branch === 'main') { // Artisan::call('deploy:production'); // } } private function handlePullRequest(array $payload): void { $action = $payload['action']; $pr = $payload['pull_request']; Log::info('GitHub: Pull request ' . $action, [ 'pr_number' => $pr['number'], 'title' => $pr['title'], 'author' => $pr['user']['login'], 'state' => $pr['state'], ]); match ($action) { 'opened' => $this->handlePullRequestOpened($pr), 'closed' => $this->handlePullRequestClosed($pr), 'reopened' => $this->handlePullRequestReopened($pr), 'synchronize' => $this->handlePullRequestSynchronize($pr), default => null, }; } private function handlePullRequestOpened(array $pr): void { // Example: Send notification to Slack // Notification::route('slack', config('services.slack.webhook')) // ->notify(new NewPullRequestNotification($pr)); } private function handlePullRequestClosed(array $pr): void { if ($pr['merged']) { Log::info('GitHub: Pull request merged', [ 'pr_number' => $pr['number'], 'merged_by' => $pr['merged_by']['login'] ?? 'unknown', ]); } else { Log::info('GitHub: Pull request closed without merge', [ 'pr_number' => $pr['number'], ]); } } private function handlePullRequestReopened(array $pr): void { Log::info('GitHub: Pull request reopened', [ 'pr_number' => $pr['number'], ]); } private function handlePullRequestSynchronize(array $pr): void { Log::info('GitHub: Pull request synchronized (new commits)', [ 'pr_number' => $pr['number'], ]); // Example: Trigger CI/CD pipeline // Artisan::call('ci:run', ['pr' => $pr['number']]); } private function handlePullRequestReview(array $payload): void { $review = $payload['review']; $pr = $payload['pull_request']; Log::info('GitHub: Pull request review submitted', [ 'pr_number' => $pr['number'], 'reviewer' => $review['user']['login'], 'state' => $review['state'], ]); } private function handleIssues(array $payload): void { $action = $payload['action']; $issue = $payload['issue']; Log::info('GitHub: Issue ' . $action, [ 'issue_number' => $issue['number'], 'title' => $issue['title'], 'author' => $issue['user']['login'], ]); } private function handleIssueComment(array $payload): void { $action = $payload['action']; $comment = $payload['comment']; $issue = $payload['issue']; Log::info('GitHub: Issue comment ' . $action, [ 'issue_number' => $issue['number'], 'commenter' => $comment['user']['login'], ]); } private function handleRelease(array $payload): void { $action = $payload['action']; $release = $payload['release']; Log::info('GitHub: Release ' . $action, [ 'tag' => $release['tag_name'], 'name' => $release['name'], 'author' => $release['author']['login'], ]); if ($action === 'published') { // Example: Deploy to production // Artisan::call('deploy:production', ['version' => $release['tag_name']]); } } private function handleWorkflowRun(array $payload): void { $workflow = $payload['workflow_run']; Log::info('GitHub: Workflow run ' . $workflow['conclusion'], [ 'workflow' => $workflow['name'], 'status' => $workflow['status'], 'conclusion' => $workflow['conclusion'], ]); } private function handleDeployment(array $payload): void { $deployment = $payload['deployment']; Log::info('GitHub: Deployment event', [ 'environment' => $deployment['environment'], 'ref' => $deployment['ref'], ]); } private function handleStar(array $payload): void { $action = $payload['action']; $repository = $payload['repository']['full_name']; $stargazer = $payload['sender']['login']; Log::info('GitHub: Repository ' . ($action === 'created' ? 'starred' : 'unstarred'), [ 'repository' => $repository, 'stargazer' => $stargazer, 'stars' => $payload['repository']['stargazers_count'], ]); } private function handleUnknownEvent(string $event, array $payload): void { Log::warning('GitHub: Unknown event type received', [ 'event_type' => $event, ]); } }
3. Webhook Flow Diagram
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
β β β β β β
β GitHub Server ββββββββββΆβ LaraWebhook ββββββββββΆβ Your Application β
β β POST β - Validates β Valid β - Process event β
β (Webhook) β β X-Hub-Signature β β - Trigger CI/CD β
β β β - Logs event β β - Send messages β
βββββββββββββββββββ β - Returns response β β β
ββββββββββββββββββββββββ βββββββββββββββββββββββ
β
β Invalid signature
βΌ
ββββββββββββββββββββββββ
β Returns 403 β
β Forbidden β
ββββββββββββββββββββββββ
4. Example Log Entry
Successful webhook processing creates a log entry:
{
"id": 2,
"service": "github",
"event": "push",
"status": "success",
"payload": {
"ref": "refs/heads/main",
"repository": {
"full_name": "username/repository",
"html_url": "https://github.com/username/repository"
},
"pusher": {
"name": "username"
},
"commits": [
{
"id": "abc123def456",
"message": "feat: add new feature",
"author": {
"name": "John Doe",
"email": "john@example.com"
}
}
]
},
"attempt": 0,
"error_message": null,
"created_at": "2024-01-15 14:25:00"
}
5. Configure Webhook in GitHub
- Go to your repository Settings β Webhooks β Add webhook
- Payload URL:
https://your-domain.com/github-webhook - Content type:
application/json - Secret: Enter a strong secret and add it to your
.envfile - Events: Select individual events or "Send me everything"
- Active: Check this box
- Click Add webhook
6. Testing & Debugging
View webhook logs:
php artisan tinker >>> \Proxynth\LaraWebhook\Models\WebhookLog::where('service', 'github')->latest()->first();
Test webhook delivery:
- Go to your repository Settings β Webhooks
- Click on your webhook
- Scroll to Recent Deliveries
- Click Redeliver on any delivery to resend it
π¬ Slack Integration
1. Configuration
Add your Slack signing secret to .env:
SLACK_WEBHOOK_SECRET=your_slack_signing_secret_here
Get your signing secret from your Slack app settings:
- Go to Slack API
- Select your app
- Go to Basic Information β App Credentials
- Copy the Signing Secret
2. Create Route and Controller
Define the webhook route in routes/web.php:
use App\Http\Controllers\SlackWebhookController; Route::post('/slack-webhook', [SlackWebhookController::class, 'handle']) ->middleware('validate-webhook:slack');
Create the controller at app/Http/Controllers/SlackWebhookController.php:
<?php namespace App\Http\Controllers; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class SlackWebhookController extends Controller { public function handle(Request $request): JsonResponse { $payload = json_decode($request->getContent(), true); // Handle URL verification challenge if (isset($payload['type']) && $payload['type'] === 'url_verification') { return response()->json(['challenge' => $payload['challenge']]); } $eventType = $payload['event']['type'] ?? $payload['type'] ?? 'unknown'; match ($eventType) { 'app_mention' => $this->handleAppMention($payload), 'message' => $this->handleMessage($payload), 'block_actions' => $this->handleBlockActions($payload), 'view_submission' => $this->handleViewSubmission($payload), default => $this->handleUnknownEvent($eventType, $payload), }; return response()->json(['status' => 'success']); } private function handleAppMention(array $payload): void { $event = $payload['event']; Log::info('Slack: App mentioned', [ 'user' => $event['user'], 'channel' => $event['channel'], 'text' => $event['text'], ]); // Example: Reply to the mention // $this->slackClient->chat->postMessage([ // 'channel' => $event['channel'], // 'text' => "Hi <@{$event['user']}>! How can I help?", // ]); } private function handleMessage(array $payload): void { $event = $payload['event']; // Ignore bot messages to prevent loops if (isset($event['bot_id'])) { return; } Log::info('Slack: Message received', [ 'channel' => $event['channel'], 'user' => $event['user'] ?? 'unknown', ]); } private function handleBlockActions(array $payload): void { $action = $payload['actions'][0] ?? []; Log::info('Slack: Block action triggered', [ 'action_id' => $action['action_id'] ?? 'unknown', 'user' => $payload['user']['id'] ?? 'unknown', ]); } private function handleViewSubmission(array $payload): void { Log::info('Slack: View submitted', [ 'view_id' => $payload['view']['id'] ?? 'unknown', 'user' => $payload['user']['id'] ?? 'unknown', ]); } private function handleUnknownEvent(string $eventType, array $payload): void { Log::warning('Slack: Unknown event type', ['event_type' => $eventType]); } }
3. Signature Validation Flow
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
β β β β β β
β Slack Server ββββββββββΆβ LaraWebhook ββββββββββΆβ Your Application β
β β POST β - Validates β Valid β - Process event β
β (Event/Action) β β X-Slack-Signature β β - Reply to users β
β β β - Checks timestamp β β - Update state β
βββββββββββββββββββ β - Logs event β β β
ββββββββββββββββββββββββ βββββββββββββββββββββββ
4. Configure Webhook in Slack
- Go to Slack API and select your app
- Navigate to Event Subscriptions (for events) or Interactivity & Shortcuts (for interactions)
- Enable the feature and enter your URL:
https://your-domain.com/slack-webhook - For events, subscribe to the events you want (e.g.,
app_mention,message.channels) - Save changes and reinstall the app if prompted
π Shopify Integration
1. Configuration
Add your Shopify webhook secret to .env:
SHOPIFY_WEBHOOK_SECRET=your_shopify_webhook_secret_here
2. Create Route and Controller
Define the webhook route in routes/web.php:
use App\Http\Controllers\ShopifyWebhookController; Route::post('/shopify-webhook', [ShopifyWebhookController::class, 'handle']) ->middleware('validate-webhook:shopify');
Create the controller at app/Http/Controllers/ShopifyWebhookController.php:
<?php namespace App\Http\Controllers; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class ShopifyWebhookController extends Controller { public function handle(Request $request): JsonResponse { $payload = json_decode($request->getContent(), true); $topic = $request->header('X-Shopify-Topic'); $shopDomain = $request->header('X-Shopify-Shop-Domain'); Log::info('Shopify webhook received', [ 'topic' => $topic, 'shop' => $shopDomain, ]); match ($topic) { 'orders/create' => $this->handleOrderCreate($payload), 'orders/updated' => $this->handleOrderUpdated($payload), 'orders/cancelled' => $this->handleOrderCancelled($payload), 'orders/fulfilled' => $this->handleOrderFulfilled($payload), 'products/create' => $this->handleProductCreate($payload), 'products/update' => $this->handleProductUpdate($payload), 'products/delete' => $this->handleProductDelete($payload), 'customers/create' => $this->handleCustomerCreate($payload), 'refunds/create' => $this->handleRefundCreate($payload), default => $this->handleUnknownTopic($topic, $payload), }; return response()->json(['status' => 'success']); } private function handleOrderCreate(array $payload): void { Log::info('Shopify: Order created', [ 'order_id' => $payload['id'], 'order_number' => $payload['order_number'], 'total_price' => $payload['total_price'], 'customer_email' => $payload['email'], ]); // Example: Sync order to your database // Order::create([ // 'shopify_id' => $payload['id'], // 'number' => $payload['order_number'], // 'total' => $payload['total_price'], // 'currency' => $payload['currency'], // 'status' => $payload['financial_status'], // ]); } private function handleOrderUpdated(array $payload): void { Log::info('Shopify: Order updated', [ 'order_id' => $payload['id'], 'financial_status' => $payload['financial_status'], ]); } private function handleOrderCancelled(array $payload): void { Log::info('Shopify: Order cancelled', [ 'order_id' => $payload['id'], 'cancel_reason' => $payload['cancel_reason'] ?? 'unknown', ]); } private function handleOrderFulfilled(array $payload): void { Log::info('Shopify: Order fulfilled', [ 'order_id' => $payload['id'], ]); } private function handleProductCreate(array $payload): void { Log::info('Shopify: Product created', [ 'product_id' => $payload['id'], 'title' => $payload['title'], ]); } private function handleProductUpdate(array $payload): void { Log::info('Shopify: Product updated', [ 'product_id' => $payload['id'], ]); } private function handleProductDelete(array $payload): void { Log::info('Shopify: Product deleted', [ 'product_id' => $payload['id'], ]); } private function handleCustomerCreate(array $payload): void { Log::info('Shopify: Customer created', [ 'customer_id' => $payload['id'], 'email' => $payload['email'], ]); } private function handleRefundCreate(array $payload): void { Log::info('Shopify: Refund created', [ 'refund_id' => $payload['id'], 'order_id' => $payload['order_id'], ]); } private function handleUnknownTopic(?string $topic, array $payload): void { Log::warning('Shopify: Unknown topic', ['topic' => $topic]); } }
3. Signature Validation Flow
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
β β β β β β
β Shopify Server ββββββββββΆβ LaraWebhook ββββββββββΆβ Your Application β
β β POST β - Validates HMAC β Valid β - Sync orders β
β (Webhook) β β X-Shopify-Hmac β β - Update inventory β
β β β - Logs event β β - Process refunds β
βββββββββββββββββββ β - Returns 200 β β β
ββββββββββββββββββββββββ βββββββββββββββββββββββ
4. Configure Webhook in Shopify
Via Shopify Admin:
- Go to Settings β Notifications β Webhooks
- Click Create webhook
- Select the event (e.g.,
Order creation) - Format: JSON
- URL:
https://your-domain.com/shopify-webhook - API version: Select the latest stable version
- Click Save
- Copy the webhook signing secret and add to your
.env
Via Shopify API:
curl -X POST "https://your-shop.myshopify.com/admin/api/2024-01/webhooks.json" \ -H "X-Shopify-Access-Token: YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook": { "topic": "orders/create", "address": "https://your-domain.com/shopify-webhook", "format": "json" } }'
5. Testing with Shopify CLI
# Install Shopify CLI npm install -g @shopify/cli @shopify/theme # Test webhook delivery shopify webhook trigger --topic orders/create \ --api-version 2024-01 \ --delivery-method http \ --address https://your-domain.com/shopify-webhook
π Async Retry Management
When a webhook validation fails, you can automatically queue it for background retry instead of returning an error immediately.
Enable Async Retries
WEBHOOK_RETRIES_ENABLED=true WEBHOOK_ASYNC_RETRIES=true WEBHOOK_MAX_ATTEMPTS=3
How It Works
- Webhook arrives, validation fails
- Middleware returns
202 Acceptedwith{"status": "accepted_for_retry"} RetryWebhookJobis dispatched to the queue with delay- Job retries validation in background
- If all retries fail, error is logged
// config/larawebhook.php 'retries' => [ 'enabled' => true, 'max_attempts' => 3, 'delays' => [1, 5, 10], // seconds between retries 'async' => true, // Enable queue-based retries ],
Response Codes
| Scenario | Response |
|---|---|
| Validation success | 200 OK + handler response |
| Duplicate webhook | 200 OK + {"status": "already_processed"} |
| Validation fails (async=true) | 202 Accepted + {"status": "accepted_for_retry"} |
| Validation fails (async=false) | 403 Forbidden |
π Laravel Logging
All webhook events are automatically logged to Laravel's logging system in addition to the database.
Log Levels
- Success:
Log::info('Webhook processed successfully', $context) - Failure:
Log::error('Webhook validation failed: {message}', $context)
Log Context
[
'service' => 'stripe',
'event' => 'payment_intent.succeeded',
'external_id' => 'evt_123',
'attempt' => 0,
]
View Logs
# Laravel default log tail -f storage/logs/laravel.log | grep -i webhook # Or use your logging channel (e.g., Papertrail, Loggly, etc.)
π Best Practices
Security
β Always use HTTPS in production
// Force HTTPS for webhook routes in production if (app()->environment('production')) { URL::forceScheme('https'); }
β Validate webhook signatures
// The validate-webhook middleware does this automatically Route::post('/webhook', [Controller::class, 'handle']) ->middleware('validate-webhook:stripe');
β Keep secrets in environment variables
# .env file (NEVER commit this file) STRIPE_WEBHOOK_SECRET=whsec_your_secret_here GITHUB_WEBHOOK_SECRET=your_github_secret_here
β Rotate secrets regularly
- Update secrets in your webhook provider dashboard
- Update
.envfile - Deploy the change
- Delete old webhook endpoint after verifying the new one works
β Limit webhook IP addresses (optional)
// Only accept webhooks from Stripe IPs Route::post('/stripe-webhook', [StripeWebhookController::class, 'handle']) ->middleware(['validate-webhook:stripe', 'throttle:60,1']);
Error Handling
β Log all webhook events
// LaraWebhook automatically logs all webhooks to the database // View them in the dashboard: /larawebhook/dashboard
β Handle failures gracefully
private function handlePaymentFailed(array $payload): void { try { // Process the event $this->processPayment($payload); } catch (\Exception $e) { // Log the error Log::error('Failed to process payment webhook', [ 'error' => $e->getMessage(), 'payload' => $payload, ]); // Notify administrators // Notification::route('slack', config('services.slack.webhook')) // ->notify(new WebhookProcessingFailed($e, $payload)); } }
β Use try-catch for external calls
private function handlePush(array $payload): void { try { // Call external service Http::timeout(5)->post('https://external-api.com/deploy', [ 'repository' => $payload['repository']['name'], ]); } catch (\Exception $e) { Log::error('Failed to trigger deployment', [ 'error' => $e->getMessage(), ]); // Don't throw - webhook should still return 200 OK } }
Performance
β Process webhooks asynchronously with queues
public function handle(Request $request): JsonResponse { $payload = json_decode($request->getContent(), true); $event = $payload['type']; // Dispatch to queue for async processing ProcessStripeWebhook::dispatch($event, $payload); // Return 200 immediately return response()->json(['status' => 'success']); }
β Set reasonable timeouts
// Don't let webhook processing block the response set_time_limit(30); // 30 seconds max
Idempotency
LaraWebhook automatically handles idempotency. The middleware extracts external IDs from webhook providers and rejects duplicates before they reach your handler.
Automatic Behavior:
- Duplicate webhooks return
200 OKwith{"status": "already_processed", "external_id": "..."} - This prevents infinite retries from providers
- Your handler only processes each webhook once
External ID Sources:
| Service | External ID Source | Example |
|---|---|---|
| Stripe | Payload id field |
evt_1234567890abcdef |
| GitHub | X-GitHub-Delivery header |
abc123-delivery-uuid |
| Slack | Payload event_id field |
Ev1234567890 |
| Shopify | X-Shopify-Webhook-Id header |
b54557e4-e9e0-4d5c-8e6b-9d2e7a8b1c3d |
β Query logs by external ID (optional)
use Proxynth\Larawebhook\Models\WebhookLog; // Find a specific webhook $log = WebhookLog::findByExternalId('stripe', 'evt_1234567890'); // Check if exists $exists = WebhookLog::existsForExternalId('stripe', 'evt_1234567890'); // Filter by external ID $logs = WebhookLog::service('github') ->externalId('abc123-delivery-id') ->get();
Note: The
external_idcolumn has a unique constraint per service, preventing duplicate entries.
Monitoring
β Monitor webhook failures
# Check for recent failures php artisan tinker >>> \Proxynth\LaraWebhook\Models\WebhookLog::where('status', 'failed') ->where('created_at', '>', now()->subHour()) ->count();
β Enable automatic failure notifications
# LaraWebhook has built-in notifications for repeated failures WEBHOOK_NOTIFICATIONS_ENABLED=true WEBHOOK_NOTIFICATION_CHANNELS=mail,slack WEBHOOK_EMAIL_RECIPIENTS=admin@example.com WEBHOOK_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... WEBHOOK_FAILURE_THRESHOLD=3
See the Failure Notifications section for complete setup.
β Use the dashboard for monitoring
- Access at
/larawebhook/dashboard - Filter by service, status, date
- Replay failed webhooks
- View detailed payloads and error messages
π» Code Examples
Ready-to-use code examples for common webhook integrations. Copy, paste, and customize!
π Examples Directory
The examples/ directory contains fully functional controller examples:
-
- Complete Stripe integration with payment intents, charges, subscriptions, and invoices
- Error handling and automatic logging
- Production-ready code with best practices
-
- Full GitHub webhook handling (push, PR, issues, releases, workflows)
- Auto-deployment on release
- Automatic retry on failure
-
- Step-by-step guide for adding custom services (Shopify example)
- Custom validator creation
- Middleware and controller setup
π Quick Start with Examples
Option 1: Copy the Full Controller
# Copy the example you need
cp vendor/proxynth/larawebhook/examples/StripeWebhookController.php \
app/Http/Controllers/StripeWebhookController.php
Option 2: Use as Reference
Open the examples and copy specific methods you need:
// From examples/StripeWebhookController.php private function handlePaymentIntentSucceeded(array $payload): void { $paymentIntent = $payload['data']['object']; // Your custom logic here $order = Order::where('stripe_payment_intent_id', $paymentIntent['id'])->first(); $order->update(['status' => 'paid']); }
π Example Usage Patterns
Pattern 1: Simple Stripe Integration
// routes/web.php Route::post('/stripe-webhook', [StripeWebhookController::class, 'handle']) ->middleware('validate-webhook:stripe'); // .env STRIPE_WEBHOOK_SECRET=whsec_your_secret_here
Pattern 2: GitHub Auto-Deploy
// From GitHubWebhookController.php private function handlePush(array $payload): void { $branch = str_replace('refs/heads/', '', $payload['ref']); if ($branch === 'main') { Artisan::call('deploy:production'); } }
Pattern 3: Custom Service (Shopify)
// 1. Create a signature validator implementing SignatureValidatorInterface class ShopifySignatureValidator implements SignatureValidatorInterface { public function validate(string $payload, string $signature, string $secret, int $tolerance = 300): bool { $calculated = base64_encode(hash_hmac('sha256', $payload, $secret, true)); if (! hash_equals($calculated, $signature)) { throw new InvalidSignatureException('Invalid Shopify signature.'); } return true; } public function serviceName(): string { return 'shopify'; } } // 2. Create a payload parser implementing PayloadParserInterface class ShopifyPayloadParser implements PayloadParserInterface { public function extractEventType(array $data): string { return $data['topic'] ?? 'unknown'; } public function extractMetadata(array $data): array { return ['shop_domain' => $data['shop_domain'] ?? null]; } public function serviceName(): string { return 'shopify'; } } // 3. Register in WebhookService enum (see Extensible Architecture section)
π Full Documentation
For detailed usage instructions, testing strategies, and best practices, see:
- Examples README - Complete guide with patterns and tips
- Integration Examples - Stripe and GitHub integration guides below
π§ Configuration
Modify config/larawebhook.php to:
- Add services (Stripe, GitHub, etc.)
- Configure validation tolerance
- Enable retry management
- Set up failure notifications
- Customize the dashboard
Example:
'services' => [ 'stripe' => [ 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'), 'tolerance' => 300, ], 'github' => [ 'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'), 'tolerance' => 300, ], ], 'retries' => [ 'enabled' => true, 'max_attempts' => 3, 'delays' => [1, 5, 10], // seconds 'async' => false, // Queue retries in background (returns 202 Accepted) ], 'notifications' => [ 'enabled' => env('WEBHOOK_NOTIFICATIONS_ENABLED', false), 'channels' => ['mail', 'slack'], 'failure_threshold' => 3, ], 'dashboard' => [ 'enabled' => true, 'path' => '/larawebhook/dashboard', ],
π Logging
Webhooks are logged in the webhook_logs table with:
- service (e.g., stripe, github)
- event (e.g., payment_intent.succeeded)
- status (success/failed)
- payload (webhook content)
- created_at
To view logs:
php artisan tinker >>> \Proxynth\LaraWebhook\Models\WebhookLog::latest()->get();
π Dashboard & API
LaraWebhook provides a modern dashboard built with Alpine.js and Tailwind CSS to visualize and manage webhook logs.
Access the Dashboard
The dashboard is automatically available at:
http://your-app.test/larawebhook/dashboard
Features:
- π Paginated webhook logs table
- π Filter by service, status, and date
- ποΈ View detailed payload and error messages
- π Replay failed webhooks
- π± Fully responsive design
API Endpoints
The package also provides REST API endpoints for programmatic access:
Get Webhook Logs
GET /api/larawebhook/logs
Query Parameters:
service- Filter by service (stripe, github, etc.)status- Filter by status (success, failed)date- Filter by date (YYYY-MM-DD)per_page- Results per page (default: 10)page- Page number
Example:
curl "https://your-app.test/api/larawebhook/logs?service=stripe&status=failed&per_page=25"
Response:
{
"data": [
{
"id": 1,
"service": "stripe",
"event": "payment_intent.succeeded",
"status": "success",
"payload": {...},
"attempt": 0,
"created_at": "01/01/2024 10:30:00"
}
],
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 10,
"total": 50
},
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
}
}
Replay a Webhook
POST /api/larawebhook/logs/{id}/replay
Example:
curl -X POST "https://your-app.test/api/larawebhook/logs/123/replay" \ -H "Content-Type: application/json"
Response:
{
"success": true,
"message": "Webhook replayed successfully!",
"log": {
"id": 124,
"service": "stripe",
"event": "payment_intent.succeeded",
"status": "success",
"attempt": 1
}
}
Dashboard Configuration
Customize the dashboard in config/larawebhook.php:
'dashboard' => [ 'enabled' => env('LARAWEBHOOK_DASHBOARD_ENABLED', true), 'path' => env('LARAWEBHOOK_DASHBOARD_PATH', '/larawebhook/dashboard'), 'middleware' => env('LARAWEBHOOK_DASHBOARD_MIDDLEWARE', 'web'), ],
Disable the dashboard:
LARAWEBHOOK_DASHBOARD_ENABLED=false
Change the dashboard path:
LARAWEBHOOK_DASHBOARD_PATH=/admin/webhooks
Add authentication middleware:
LARAWEBHOOK_DASHBOARD_MIDDLEWARE=web,auth
Dashboard Screenshots
π Failure Notifications
LaraWebhook can automatically notify you when webhooks fail repeatedly. Get alerted via Email and Slack when a service experiences multiple consecutive failures.
Why Notifications?
- Detect outages early: Know immediately when a webhook provider has issues
- Reduce downtime: React quickly to integration problems
- Team collaboration: Send alerts to Slack channels for instant visibility
Configuration
Enable notifications in config/larawebhook.php:
'notifications' => [ // Enable/disable failure notifications 'enabled' => env('WEBHOOK_NOTIFICATIONS_ENABLED', true), // Notification channels (mail, slack) 'channels' => array_filter(explode(',', env('WEBHOOK_NOTIFICATION_CHANNELS', 'mail'))), // Slack webhook URL (create an Incoming Webhook in your Slack app) 'slack_webhook' => env('WEBHOOK_SLACK_WEBHOOK_URL'), // Email recipients for failure notifications 'email_recipients' => array_filter(explode(',', env('WEBHOOK_EMAIL_RECIPIENTS', ''))), // Number of consecutive failures before sending notification 'failure_threshold' => (int) env('WEBHOOK_FAILURE_THRESHOLD', 3), // Time window in minutes to count failures 'failure_window_minutes' => (int) env('WEBHOOK_FAILURE_WINDOW', 30), // Cooldown in minutes between notifications for the same service/event 'cooldown_minutes' => (int) env('WEBHOOK_NOTIFICATION_COOLDOWN', 30), ],
Environment Variables
Add these to your .env file:
# Enable notifications WEBHOOK_NOTIFICATIONS_ENABLED=true # Channels: mail, slack (comma-separated) WEBHOOK_NOTIFICATION_CHANNELS=mail,slack # Email recipients (comma-separated) WEBHOOK_EMAIL_RECIPIENTS=admin@example.com,devops@example.com # Slack incoming webhook URL WEBHOOK_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL # Number of failures before alerting (default: 3) WEBHOOK_FAILURE_THRESHOLD=3 # Time window for counting failures in minutes (default: 30) WEBHOOK_FAILURE_WINDOW=30 # Cooldown between notifications in minutes (default: 30) WEBHOOK_NOTIFICATION_COOLDOWN=30
How It Works
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
β β β β β β
β Webhook Fails ββββββΆβ FailureDetector ββββββΆβ Send Notification β
β (3rd time) β β - Count failures β β - Email β
β β β - Check threshold β β - Slack β
β β β - Check cooldown β β β
βββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββββββ
β
β Below threshold
β or in cooldown
βΌ
ββββββββββββββββββββββββ
β No notification β
β (prevents spam) β
ββββββββββββββββββββββββ
- Failure Detection: Counts consecutive failures for each service/event combination
- Threshold Check: Only triggers notification after N failures (configurable)
- Time Window: Only counts failures within the last X minutes
- Cooldown: Prevents notification spam by waiting between alerts
Slack Setup
- Go to Slack API
- Click Create New App β From scratch
- Give your app a name and select your workspace
- Go to Incoming Webhooks and toggle it On
- Click Add New Webhook to Workspace
- Select the channel for notifications (e.g.,
#alertsor#monitoring) - Copy the webhook URL and add it to your
.envfile
Webhook URL format: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX
Email Notifications
Email notifications use Laravel's built-in mail system. Make sure your mail configuration is set up in .env:
MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=your_username MAIL_PASSWORD=your_password MAIL_FROM_ADDRESS=noreply@example.com MAIL_FROM_NAME="LaraWebhook"
Notification Content
Email Notification:
- Subject:
Webhook Failure Alert: {service} - Service and event name
- Number of consecutive failures
- Last attempt timestamp
- Error message (if available)
- Link to dashboard
Slack Notification:
- Red alert color (danger level)
- Service and event details
- Failure count
- Error message
- Direct link to dashboard
Events
LaraWebhook dispatches an event when a notification is sent, allowing you to add custom logic:
use Proxynth\Larawebhook\Events\WebhookNotificationSent; // In your EventServiceProvider protected $listen = [ WebhookNotificationSent::class => [ YourCustomListener::class, ], ]; // Your listener class YourCustomListener { public function handle(WebhookNotificationSent $event): void { // $event->log - The WebhookLog model // $event->failureCount - Number of failures // Example: Log to external monitoring service Http::post('https://monitoring.example.com/webhook-failure', [ 'service' => $event->log->service, 'event' => $event->log->event, 'failures' => $event->failureCount, ]); } }
Preventing Notification Spam
LaraWebhook includes built-in spam prevention:
- Failure Threshold: Only notifies after N consecutive failures (default: 3)
- Time Window: Only counts failures within the last X minutes (default: 30)
- Cooldown Period: Won't send another notification for the same service/event within X minutes (default: 30)
Example scenario:
- Stripe
payment.failedfails 3 times in 10 minutes β Notification sent - 5 more failures in the next 20 minutes β No notification (cooldown active)
- After 30 minutes, 3 more failures β Notification sent again
Disabling Notifications
To completely disable notifications:
WEBHOOK_NOTIFICATIONS_ENABLED=false
Or to disable only for certain environments, use Laravel's configuration:
// config/larawebhook.php 'notifications' => [ 'enabled' => env('WEBHOOK_NOTIFICATIONS_ENABLED', app()->environment('production')), // ... other settings ],
π§ͺ Tests
Run tests with:
composer test
(Tests cover validation, retries, and logging.)
π Release Process
This project uses Release Please for automated releases and changelog management.
How it works
-
Commit with Conventional Commits format:
git commit -m "feat: add new webhook validation" git commit -m "fix: resolve signature verification bug" git commit -m "docs: update installation instructions"
-
Release Please creates a PR automatically when changes are pushed to
master:- Generates/updates
CHANGELOG.mdbased on commits - Bumps version in
.release-please-manifest.json - Creates a release PR titled "chore(master): release X.Y.Z"
- Generates/updates
-
Review and merge the release PR:
- Review the generated changelog
- Merge the PR to trigger the release
-
Automatic actions on merge:
- Creates a GitHub Release with tag
vX.Y.Z - Runs tests and static analysis
- Packagist syncs automatically (no manual webhook needed)
- Creates a GitHub Release with tag
Conventional Commits format
feat:β New feature (bumps minor version)fix:β Bug fix (bumps patch version)docs:β Documentation changesstyle:β Code style changes (formatting, etc.)refactor:β Code refactoringperf:β Performance improvementstest:β Adding/updating testschore:β Maintenance tasksci:β CI/CD changes
Breaking changes: Add ! after type or add BREAKING CHANGE: in commit body to bump major version.
Example:
git commit -m "feat!: change webhook validation API"
π€ Contributing
- Fork the repository
- Create a branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -am 'Add my feature') - Push the branch (
git push origin feature/my-feature) - Open a Pull Request
(See CONTRIBUTING.md for more details.)
π License
This project is licensed under the MIT License. See LICENSE for more information.




