Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Webhook Client Laravel Package

spatie/laravel-webhook-client

Receive webhooks in your Laravel app with Spatie’s webhook client. Verify signed requests, store incoming payloads, and process them asynchronously via queued jobs. Includes configurable webhook profiles and processing logic for reliable integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. Installation:

    composer require spatie/laravel-webhook-client
    php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"
    php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
    php artisan migrate
    
  2. Configure .env:

    WEBHOOK_CLIENT_SECRET=your_webhook_secret_here
    
  3. Define Route:

    Route::webhooks('webhook-receiving-url');
    
  4. Exclude CSRF for Webhook Route (Laravel 10+):

    // app/Http/Middleware/VerifyCsrfToken.php
    protected $except = ['webhook-receiving-url'];
    
  5. Create a Job (e.g., app/Jobs/ProcessWebhookJob.php):

    namespace App\Jobs;
    
    use Spatie\WebhookClient\Jobs\ProcessWebhookJob as SpatieProcessWebhookJob;
    
    class ProcessWebhookJob extends SpatieProcessWebhookJob
    {
        public function handle()
        {
            $payload = $this->webhookCall->payload;
            // Process payload (e.g., parse JSON, trigger events, etc.)
        }
    }
    
  6. Update Config (config/webhook-client.php):

    'process_webhook_job' => App\Jobs\ProcessWebhookJob::class,
    
  7. Test with a Webhook Sender: Use tools like webhook.site or Postman to send a signed request to your endpoint.


First Use Case: Handling Stripe Webhooks

  1. Configure Stripe Secret:

    WEBHOOK_CLIENT_SECRET=stripe_webhook_signing_secret
    
  2. Create a Job for Stripe Events:

    namespace App\Jobs;
    
    use Spatie\WebhookClient\Jobs\ProcessWebhookJob;
    
    class HandleStripeWebhook extends ProcessWebhookJob
    {
        public function handle()
        {
            $payload = json_decode($this->webhookCall->payload, true);
            $event = $payload['data']['object'];
    
            // Handle Stripe events (e.g., payment_intent.succeeded)
            if ($event['object'] === 'payment_intent' && $event['status'] === 'succeeded') {
                // Update user subscription, send email, etc.
            }
        }
    }
    
  3. Update Config:

    'process_webhook_job' => App\Jobs\HandleStripeWebhook::class,
    
  4. Send a Test Webhook: Use Stripe CLI or Postman to trigger a test event (e.g., payment_intent.succeeded).


Implementation Patterns

Core Workflow

  1. Incoming Request:

    • Route receives a POST request to /webhook-receiving-url.
    • Package validates the signature using DefaultSignatureValidator (HMAC-SHA256 by default).
  2. Profile Check:

    • Uses ProcessEverythingWebhookProfile (default) to determine if the request should be processed.
    • Override with a custom profile (e.g., filter by X-Event-Type header) for selective processing.
  3. Storage:

    • Saves payload, headers (configurable via store_headers), and metadata to webhook_calls table.
    • Example custom storage (extend WebhookCall):
      namespace App\Models;
      
      use Spatie\WebhookClient\Models\WebhookCall;
      
      class CustomWebhookCall extends WebhookCall
      {
          protected $table = 'custom_webhook_calls';
      
          public static function storeWebhook($payload, $headers, $config)
          {
              return static::create([
                  'payload' => $payload,
                  'headers' => $headers,
                  // Custom fields
                  'source_app' => $headers['x-source-app'] ?? 'unknown',
              ]);
          }
      }
      
      Update config:
      'webhook_model' => App\Models\CustomWebhookCall::class,
      
  4. Queue Processing:

    • Dispatches ProcessWebhookJob to handle business logic asynchronously.
    • Example job with error handling:
      public function handle()
      {
          try {
              $payload = json_decode($this->webhookCall->payload, true);
              // Business logic here
          } catch (\Exception $e) {
              $this->webhookCall->update(['exception' => $e->getMessage()]);
              // Optionally notify admins via Slack/email
          }
      }
      
  5. Response:

    • Default: Returns 200 OK with OK message.
    • Custom response (e.g., for idempotency):
      namespace App\WebhookResponses;
      
      use Spatie\WebhookClient\WebhookResponse\RespondsToWebhook;
      use Illuminate\Http\Request;
      use Spatie\WebhookClient\WebhookConfig;
      
      class CustomResponse implements RespondsToWebhook
      {
          public function respondToValidWebhook(Request $request, WebhookConfig $config)
          {
              return response()->json(['status' => 'processed', 'idempotency_key' => $request->header('idempotency-key')]);
          }
      }
      
      Update config:
      'webhook_response' => App\WebhookResponses\CustomResponse::class,
      

Integration Tips

  1. Multiple Webhook Sources: Configure multiple endpoints in webhook-client.php:

    'configs' => [
        [
            'name' => 'stripe',
            'signing_secret' => env('STRIPE_WEBHOOK_SECRET'),
            'process_webhook_job' => App\Jobs\HandleStripeWebhook::class,
        ],
        [
            'name' => 'github',
            'signing_secret' => env('GITHUB_WEBHOOK_SECRET'),
            'process_webhook_job' => App\Jobs\HandleGithubWebhook::class,
        ],
    ],
    

    Route them separately:

    Route::webhooks('stripe-webhook', 'stripe');
    Route::webhooks('github-webhook', 'github');
    
  2. Dynamic Secret Management: Use Laravel's env() or a secrets manager (e.g., AWS Secrets Manager) for secrets. Avoid hardcoding.

  3. Idempotency: Store and check idempotency-key headers in WebhookCall to prevent duplicate processing:

    public static function storeWebhook($payload, $headers, $config)
    {
        $key = $headers['idempotency-key'] ?? null;
        if ($key && static::where('idempotency_key', $key)->exists()) {
            return null; // Skip duplicate
        }
        return static::create([
            'payload' => $payload,
            'headers' => $headers,
            'idempotency_key' => $key,
        ]);
    }
    
  4. Testing: Use WebhookCall factory or seeders to simulate webhooks:

    // tests/Feature/WebhookTest.php
    public function test_stripe_webhook()
    {
        $payload = file_get_contents(__DIR__.'/stripe-payment-success.json');
        $headers = ['Signature' => 't=123,...'];
    
        $response = $this->postJson('/stripe-webhook', $payload, $headers);
    
        $response->assertOk();
        $this->assertDatabaseHas('webhook_calls', ['payload' => $payload]);
    }
    
  5. Monitoring:

    • Log webhook_calls with exception field for debugging.
    • Use Laravel Horizon to monitor queued jobs.
    • Example logging in job:
      \Log::info('Processed webhook', [
          'id' => $this->webhookCall->id,
          'payload' => $this->webhookCall->payload,
      ]);
      

Gotchas and Tips

Pitfalls

  1. Signature Mismatch:

    • Issue: Invalid signature throws a 500 error by default.
    • Fix: Customize the response in webhook_response to return 401 Unauthorized:
      public function respondToValidWebhook(Request $request, WebhookConfig $config)
      {
          return response()->json(['error' => 'Invalid signature'], 401);
      }
      
    • Debugging: Log the computed vs. received signature:
      $computed = hash_hmac('sha256', $request->getContent(), $config->signingSecret);
      \Log::error("Signature mismatch. Computed: {$computed}, Received: {$request->header('Signature')}");
      
  2. Queue Failures:

    • Issue: Jobs may fail silently if the queue worker crashes.
    • Fix: Use failed() method in jobs to handle failures:
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle
atriumphp/atrium
sandermuller/package-boost-laravel
sandermuller/boost-skills
redaxo/core
yusufgenc/filament-api-forge
l3aro/rating-star-for-filament
leek/filament-subtenant-scope
anil/file-picker
broqit/fields-ai