/laravel-agent:webhook:make
Create webhook handlers for external services (Stripe, GitHub, etc.)
Overview
The /webhook:make command generates secure webhook handlers for processing incoming webhooks from external services. It creates controllers with signature verification, event handlers for specific webhook events, middleware for security, and all necessary routing configuration.
Usage
/laravel-agent:webhook:make <ServiceName> [--events=<event1,event2>]
Examples
# Create a Stripe webhook handler with default events
/laravel-agent:webhook:make Stripe
# Create a GitHub webhook handler with specific events
/laravel-agent:webhook:make GitHub --events=push,pull_request
# Create a Paddle webhook handler
/laravel-agent:webhook:make Paddle
# Create a custom webhook handler
/laravel-agent:webhook:make Custom
Supported Services
The command includes pre-configured templates for popular webhook providers:
| Service | Verification | Common Events |
|---|---|---|
| Stripe | Signature | payment_intent.succeeded, customer.subscription.*, invoice.* |
| Paddle | Signature | subscription.created, payment.completed |
| GitHub | Signature | push, pull_request, issues |
| GitLab | Token | push, merge_request |
| Twilio | Signature | message.received |
| SendGrid | Signature | email.delivered, email.bounced |
| Custom | Configurable | User-defined |
What Gets Created
A complete webhook handler includes the following components:
| Component | Location | Description |
|---|---|---|
| Webhook Controller | app/Http/Controllers/Webhooks/ |
Receives and routes webhook events |
| Signature Middleware | app/Http/Middleware/ |
Verifies webhook authenticity |
| Event Handlers | app/Webhooks/{Service}/ |
Processes specific webhook events |
| Laravel Event | app/Events/ |
For logging and auditing webhooks |
| Webhook Routes | routes/webhooks.php |
Routes with CSRF exemption |
Example Output Structure
For /laravel-agent:webhook:make Stripe:
app/
├── Http/
│ ├── Controllers/Webhooks/
│ │ └── StripeWebhookController.php
│ └── Middleware/
│ └── VerifyStripeSignature.php
├── Webhooks/Stripe/
│ ├── PaymentIntentSucceededHandler.php
│ ├── CustomerSubscriptionCreatedHandler.php
│ ├── CustomerSubscriptionDeletedHandler.php
│ ├── CustomerSubscriptionUpdatedHandler.php
│ ├── InvoicePaidHandler.php
│ └── InvoicePaymentFailedHandler.php
├── Events/
│ └── StripeWebhookReceived.php
└── routes/
└── webhooks.php
Generated Webhook Controller
Example of a generated Stripe webhook controller:
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Webhooks;
use App\Http\Controllers\Controller;
use App\Events\StripeWebhookReceived;
use App\Webhooks\Stripe\PaymentIntentSucceededHandler;
use App\Webhooks\Stripe\CustomerSubscriptionCreatedHandler;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
final class StripeWebhookController extends Controller
{
private array $handlers = [
'payment_intent.succeeded' => PaymentIntentSucceededHandler::class,
'customer.subscription.created' => CustomerSubscriptionCreatedHandler::class,
'customer.subscription.deleted' => CustomerSubscriptionDeletedHandler::class,
'invoice.paid' => InvoicePaidHandler::class,
];
public function __invoke(Request $request): Response
{
$payload = $request->all();
$eventType = $payload['type'] ?? null;
Log::info('Stripe webhook received', [
'type' => $eventType,
'id' => $payload['id'] ?? null,
]);
// Dispatch event for logging/auditing
event(new StripeWebhookReceived($eventType, $payload));
// Handle the event
if (isset($this->handlers[$eventType])) {
try {
$handler = app($this->handlers[$eventType]);
$handler->handle($payload);
} catch (\Exception $e) {
Log::error('Stripe webhook handler failed', [
'type' => $eventType,
'error' => $e->getMessage(),
]);
// Return 500 for unexpected errors to trigger retry
if ($this->shouldRetry($e)) {
return response('', 500);
}
}
}
return response('', 200);
}
private function shouldRetry(\Exception $e): bool
{
// Retry on transient errors (database, network)
return $e instanceof \Illuminate\Database\QueryException
|| $e instanceof \Illuminate\Http\Client\ConnectionException;
}
}
Signature Verification Middleware
Example Stripe signature verification middleware:
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Stripe\Webhook;
use Stripe\Exception\SignatureVerificationException;
final class VerifyStripeSignature
{
public function handle(Request $request, Closure $next): Response
{
$signature = $request->header('Stripe-Signature');
$secret = config('services.stripe.webhook_secret');
if (!$signature || !$secret) {
abort(401, 'Missing signature or secret');
}
try {
Webhook::constructEvent(
$request->getContent(),
$signature,
$secret
);
} catch (SignatureVerificationException $e) {
abort(401, 'Invalid signature');
}
return $next($request);
}
}
Event Handler Example
Example handler for processing successful payments:
<?php
declare(strict_types=1);
namespace App\Webhooks\Stripe;
use App\Models\Order;
use App\Models\Payment;
use Illuminate\Support\Facades\Log;
final class PaymentIntentSucceededHandler
{
public function handle(array $payload): void
{
$paymentIntent = $payload['data']['object'];
$orderId = $paymentIntent['metadata']['order_id'] ?? null;
if (!$orderId) {
Log::warning('Payment intent without order_id', [
'payment_intent_id' => $paymentIntent['id'],
]);
return;
}
$order = Order::find($orderId);
if (!$order) {
Log::error('Order not found for payment intent', [
'order_id' => $orderId,
'payment_intent_id' => $paymentIntent['id'],
]);
return;
}
// Record payment
$payment = Payment::create([
'order_id' => $order->id,
'stripe_payment_intent_id' => $paymentIntent['id'],
'amount_cents' => $paymentIntent['amount'],
'currency' => $paymentIntent['currency'],
'status' => 'succeeded',
]);
// Update order status
$order->update([
'payment_status' => 'paid',
'paid_at' => now(),
]);
// Dispatch follow-up events
event(new \App\Events\OrderPaid($order));
Log::info('Payment processed successfully', [
'order_id' => $order->id,
'payment_id' => $payment->id,
]);
}
}
GitHub Webhook Example
Example GitHub webhook controller with event handling:
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Webhooks;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
final class GitHubWebhookController extends Controller
{
public function __invoke(Request $request): Response
{
$event = $request->header('X-GitHub-Event');
$payload = $request->all();
match ($event) {
'push' => $this->handlePush($payload),
'pull_request' => $this->handlePullRequest($payload),
'issues' => $this->handleIssues($payload),
default => null,
};
return response('', 200);
}
private function handlePush(array $payload): void
{
$ref = $payload['ref'];
$commits = $payload['commits'] ?? [];
$repository = $payload['repository']['full_name'];
// Trigger deployment, notify team, etc.
}
private function handlePullRequest(array $payload): void
{
$action = $payload['action'];
$pr = $payload['pull_request'];
// Handle opened, closed, merged, etc.
}
private function handleIssues(array $payload): void
{
$action = $payload['action'];
$issue = $payload['issue'];
// Handle opened, closed, labeled, etc.
}
}
GitHub Signature Verification
Example GitHub signature verification middleware:
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class VerifyGitHubSignature
{
public function handle(Request $request, Closure $next): Response
{
$signature = $request->header('X-Hub-Signature-256');
$secret = config('services.github.webhook_secret');
if (!$signature || !$secret) {
abort(401);
}
$expectedSignature = 'sha256=' . hash_hmac(
'sha256',
$request->getContent(),
$secret
);
if (!hash_equals($expectedSignature, $signature)) {
abort(401, 'Invalid signature');
}
return $next($request);
}
}
Routes Configuration
Webhook routes with signature verification middleware:
// routes/webhooks.php
use App\Http\Controllers\Webhooks\StripeWebhookController;
use App\Http\Controllers\Webhooks\GitHubWebhookController;
use App\Http\Middleware\VerifyStripeSignature;
use App\Http\Middleware\VerifyGitHubSignature;
Route::post('/webhooks/stripe', StripeWebhookController::class)
->middleware(VerifyStripeSignature::class)
->name('webhooks.stripe');
Route::post('/webhooks/github', GitHubWebhookController::class)
->middleware(VerifyGitHubSignature::class)
->name('webhooks.github');
CSRF exemption configuration:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhooks/*',
]);
})
Environment Variables
Required environment configuration for webhook secrets:
# Stripe
STRIPE_WEBHOOK_SECRET=whsec_...
# GitHub
GITHUB_WEBHOOK_SECRET=your-secret
# Paddle
PADDLE_WEBHOOK_SECRET=pdl_...
Options
Interactive prompts guide you through the setup process:
- Service Selection - Choose from Stripe, Paddle, GitHub, GitLab, Twilio, SendGrid, or Custom
- Event Selection - Select which webhook events to handle (based on service)
- Signature Verification - Enable or disable signature verification (recommended: Yes)
- Event Dispatching - Fire Laravel events for each webhook for logging/auditing
Testing Webhooks
Test your webhook handlers locally:
# Stripe CLI for local testing
stripe listen --forward-to localhost:8000/webhooks/stripe
# Trigger test event
stripe trigger payment_intent.succeeded
# GitHub CLI for testing
gh webhook forward --repo=owner/repo --events=push --url=http://localhost:8000/webhooks/github
Best Practices
- Always verify signatures - Protect against unauthorized webhook requests
- Use idempotent handlers - Handle duplicate webhook deliveries gracefully
- Return 200 quickly - Process webhooks asynchronously in queued jobs for long operations
- Log everything - Maintain detailed logs for debugging webhook issues
- Handle retries properly - Return 500 for transient errors, 200 for handled errors
- Test webhook secrets - Verify environment variables are set correctly before going live
- Monitor webhook endpoints - Track success rates and error patterns
Next Steps
After generating webhook handlers:
- Add webhook secret to your
.envfile - Configure the webhook URL in the service's dashboard (e.g.,
https://yoursite.com/webhooks/stripe) - Test using the service's CLI or testing tools
- Monitor application logs for incoming webhook events
- Implement additional event handlers as needed
See Also
- /laravel-agent:event:make - Create custom Laravel events
- /laravel-agent:middleware:make - Generate middleware
- /laravel-agent:test:make - Generate webhook tests