proxynth/larawebhook
Composer 安装命令:
composer require 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!
README 文档
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.
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
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.
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
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
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
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
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 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
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
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
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
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.
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
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
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
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
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:
| Mode | Description |
|---|---|
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,
redactedmode avoids storing raw payloads until the redaction engine is fully available.
Retention policy
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
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:
| Unit | Meaning |
|---|---|
| d | days |
| h | hours |
| m | minutes |
To preview how many logs would be deleted without deleting them:
php artisan larawebhook:prune --older-than=30d --dry-run
Scheduler exemple
You can schedule pruning in your Laravel application:
use Illuminate\Support\Facades\Schedule; Schedule::command('larawebhook:prune')->daily();
Sensitive data redaction
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
- 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
- ✅ Runs
ReceiveWebhookto validate, deduplicate, and record the webhook - ✅ Dispatches collected domain events through the Laravel adapter
- ✅ 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\Shared\Infrastructure\Laravel\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 through the application layer $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); } }
Deduplication
LaraWebhook separates audit logs from deduplication state.
webhook_logsstores the audit trail and dashboard/API read data.processed_webhook_eventsstores successfully processed webhook identities.- Deduplication uses
service + idempotency_key. external_idremains the provider delivery/event identifier.
🎯 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\Shared\Infrastructure\Laravel\Facades\Larawebhook; // Validate a webhook Larawebhook::validate($payload, $signature, 'stripe'); // Validate and log $log = Larawebhook::validateAndLog($payload, $signature, 'github', 'push'); // Legacy compatibility helpers, deprecated Larawebhook::logSuccess('stripe', 'payment.succeeded', $payload); Larawebhook::logFailure('stripe', 'payment.failed', $payload, 'Card declined'); // Prefer validateAndLog() or the corresponding application use case for new code. // 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 supported service identifiers and string conversion:
use Proxynth\Larawebhook\Shared\Domain\Enums\WebhookService; // Available services WebhookService::Stripe; // 'stripe' WebhookService::Github; // 'github' // 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']
Provider-specific signature headers, parsers, secrets, and validators are resolved internally by the ingestion infrastructure and application ports.
Using Enum with Facade
All facade methods accept both strings and the enum:
use Proxynth\Larawebhook\Shared\Infrastructure\Laravel\Facades\Larawebhook; use Proxynth\Larawebhook\Shared\Domain\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\Ingestion\Application\Ports\WebhookPayloadParserResolver; use Proxynth\Larawebhook\Shared\Domain\Enums\WebhookService; $payload = json_decode($request->getContent(), true); $parserResolver = app(WebhookPayloadParserResolver::class); // Extract event type $eventType = $parserResolver->forService(WebhookService::Stripe)->extractEventType($payload); // Returns: 'payment_intent.succeeded' // Extract metadata $metadata = $parserResolver->forService(WebhookService::Github)->extractMetadata($payload); // Returns: ['delivery_id' => '...', 'action' => 'opened', 'sender' => 'octocat', ...]
Using Validators Directly
For advanced use cases, you can use validators directly:
use Proxynth\Larawebhook\Ingestion\Infrastructure\Validation\WebhookValidatorFactory; use Proxynth\Larawebhook\Shared\Domain\Enums\WebhookService; $validator = app(WebhookValidatorFactory::class); $isValid = $validator->forService(WebhookService::Stripe)->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\Audit\Infrastructure\Laravel\Persistence\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\Audit\Infrastructure\Laravel\Persistence\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
The idempotency resolution logic is now isolated internally so it can evolve without changing the public API.
external_id keeps the provider delivery or event identifier. idempotency_key keeps the actual deduplication key, which may be the external id or a payload-hash fallback when the provider does not expose a stable identifier.
Idempotency fallback
By default, LaraWebhook uses the provider external event ID as the idempotency key.
If no external ID is available, LaraWebhook falls back to a deterministic SHA-256 hash of the normalized payload.
Object keys are sorted recursively before hashing, so equivalent JSON objects produce the same idempotency key even if
their keys are ordered differently.
List order remains significant.
The generated fallback key is prefixed with payload_hash:.
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\Audit\Infrastructure\Laravel\Persistence\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
idempotency_keycolumn has a unique constraint per service, preventing duplicate entries.external_idremains a provider identifier and may be null.
Monitoring
✅ Monitor webhook failures
# Check for recent failures php artisan tinker >>> \Proxynth\Larawebhook\Audit\Infrastructure\Laravel\Persistence\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\Audit\Infrastructure\Laravel\Persistence\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\Audit\Infrastructure\Laravel\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"
Architecture
LaraWebhook follows a pragmatic architecture inspired by Domain Driven Design, hexagonal architecture and CQRS.
The goal is to keep the package readable, testable and explicit without introducing unnecessary abstractions.
See docs/architecture.md for details.
🤝 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.
proxynth/larawebhook 适用场景与选型建议
proxynth/larawebhook 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 4 次下载、GitHub Stars 达 1, 最近一次更新时间为 2026 年 03 月 30 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「github」 「stripe」 「shopify」 「laravel」 「webhooks」 「slack」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 proxynth/larawebhook 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 proxynth/larawebhook 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 proxynth/larawebhook 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Handles basic OAuth/OAuth2 authentication along with classes for common services
Flexible payment integration for Laravel, lunarphp, supporting multiple payment providers.
MediaWiki extension that allows embedding external content, specified by URL, into your wiki pages
Rich payment solutions for Laravel framework. Paypal, payex, authorize.net, be2bill, omnipay, recurring paymens, instant notifications and many more
Shopify package for Laravel to aide in app development
统计信息
- 总下载量: 4
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 1
- 点击次数: 31
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-03-30




