Weave 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 Webhooks Laravel Package

juststeveking/laravel-webhooks

A Laravel package for receiving and processing incoming webhooks. Define webhook endpoints, validate request signatures from providers, and dispatch payloads to dedicated handler classes, keeping controllers clean and integrations secure.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and verify the baseline first. The package has not shipped since May 2023 and is archived, so before anything else confirm it installs cleanly on your target stack:

composer require juststeveking/laravel-webhooks
composer show juststeveking/laravel-webhooks   # read the illuminate/* and php constraints

A 2023-05 release means it was built against Laravel 9–10 / PHP 8.1–8.2. On Laravel 11/12 you will either get a constraint conflict or need --ignore-platform-reqs-style overrides — and overriding constraints on an archived package is the moment you forked it. Decide that consciously, not by accident. If it installs without override, you are on a supported combination and can proceed.

Publish and own the config and migrations:

php artisan vendor:publish --tag="webhooks-config"
php artisan vendor:publish --tag="webhooks-migrations"
php artisan migrate

Read the published config file top to bottom before writing any application code — the endpoint registration shape (slug → handler/job class) is the whole API of this package, and it is easier to learn from the config than from source. Read the migrations too: they define the table names you now own and that will collide with nothing only if you checked.

Find the extension point. The package is a few hundred lines across a handful of classes. Read them in this order: service provider (how routes are registered and from where), the controller that receives the request (what it does before queuing), the job contract (what your handler gets handed). That is a 20-minute exercise and it is the difference between "I understand this" and "it worked in the spike."

First use case: pick a low-stakes provider — an internal service or a sandbox Slack/GitHub hook — register one endpoint in config pointing at a job class you write, and confirm end-to-end locally using expose/ngrok plus php artisan queue:work. You want to see three things work before building on it: the request is accepted and persisted, your job receives the payload, and the provider stops retrying because you returned 2xx fast.

Implementation Patterns

One job class per provider, not one god-handler. The package hands off to a queued job; keep each provider's translation logic isolated:

// config/webhooks.php
'endpoints' => [
    'stripe' => [
        'job' => \App\Webhooks\HandleStripeWebhook::class,
        // secret / signature config as the package expects
    ],
    'github' => [
        'job' => \App\Webhooks\HandleGitHubWebhook::class,
    ],
],

Inside the job: read the persisted payload, map the provider's event type to a domain action, dispatch domain events or commands. Do not do the domain work inline in the job — the job is adapter code, and adapter code is what changes when a provider bumps their API version.

Treat persistence as the audit log and the replay mechanism. The package's real day-to-day value is that every inbound payload and its headers are stored. That gives you two things you should lean on deliberately:

  • Debugging: when a partner says "we sent it and nothing happened," you have the payload, the headers, and the request time — no reproduction required.
  • Backfill: if your handler had a bug, you re-dispatch the job for the stored record rather than asking the provider to resend. Write a small artisan command for this on day one; it costs 15 lines and it is the single highest-value thing you will add to the package.

Verify signatures against the raw body, in the package's hook, before anything else. Signature verification must run on the exact bytes received — $request->getContent() — not on the re-encoded JSON. Order matters: verify, then persist, then queue. If the package's verification hook fires after decode you will see intermittent failures on payloads containing unicode escapes or key-order-sensitive signing schemes.

Make the handler idempotent — this is non-negotiable. Every serious provider retries on timeout and on any non-2xx. Assume at-least-once delivery and dedupe on the provider's event id:

// in your job
if (ProcessedWebhook::where('provider_event_id', $payload['id'])->exists()) {
    return;
}
DB::transaction(function () use ($payload) {
    ProcessedWebhook::create(['provider_event_id' => $payload['id']]);
    // domain work
});

Register webhook routes outside your auth and CSRF middleware. If the package registers routes for you, confirm they are in a group with VerifyCsrfToken excluded (Laravel 10 and below) / CSRF middleware not applied (11+, bootstrap/app.php). This is the most common "works on my machine, 419 in staging" failure. If the package does not register routes the way you need, register them yourself and point at the package controller — do not fight the service provider.

Local development and testing. Two patterns that pay for themselves immediately:

// Feature test: fake the queue, post a realistic signed payload
Queue::fake();
$payload = file_get_contents(base_path('tests/Fixtures/stripe/order.created.json'));
$signature = hash_hmac('sha256', $payload, config('webhooks.endpoints.stripe.secret'));

$this->call('POST', '/webhooks/stripe', [], [], [], [
    'HTTP_X-Signature' => $signature,
    'CONTENT_TYPE' => 'application/json',
], $payload)
    ->assertOk();

Queue::assertPushed(HandleStripeWebhook::class);

Keep a fixtures directory of real captured payloads per provider. Replay attacks, signature mismatch, and schema changes are all caught by fixtures and by nothing else. Run php artisan queue:work --tries=1 locally so failures surface as failures instead of silently retrying three times.

Integration tip: keep the queue worker's timeout under the provider's retry window. A job that holds longer than the provider's timeout will get the same webhook again while the first is still running. Keep handler jobs short and fan out to separate jobs for slow work.

Gotchas and Tips

Archived means you own it. Pin to a commit, and if you are using this in anything with a customer or a contract behind it, fork to an internal namespace and take the migrations as yours. There will be no security patches, no Laravel major bumps, and no one to escalate to. For a spike or an internal tool that is fine; for a payment provider's webhook endpoint it is not.

Laravel version support is the day-one kill criterion. Check the illuminate/* constraints in composer.json against your app before you invest in anything. If you must override them, you have started an unplanned fork — treat everything after that as internal code with a named owner.

Config caching bites during development. Editing a published config and seeing no change is almost always config:cache stale. php artisan config:clear after endpoint changes; remember to re-cache on deploy.

Table names and config keys are collision surface. The migrations name tables in your schema and the config namespace is global. Grep your existing packages for the same model/table names before migrating, and rename in the published migration rather than after data exists.

Persisted payloads are PII. You are storing third-party request bodies indefinitely by default. Decide a retention window, add a prune command, and check whether the package encrypts at rest (assume it does not). The audit log is valuable; an unbounded PII archive is a liability.

Timeouts and payload size limits. Large payloads can trip post_max_size / max_input_vars, and the response must return fast — everything real work happens in the queue. If a provider sends a big batch event and your handler job is slow, you will see duplicate processing, which loops back to idempotency.

Debugging order of operations. When a webhook "doesn't work," check in this order: (1) is there a persisted record? If no — routing, middleware, CSRF, or signature rejection. (2) Is there a record but no domain effect? — queue worker not running, job failed, or exception swallowed. (3) Domain effect happened twice? — missing idempotency key. failed_jobs plus the persisted payload answers 90% of these in under a minute; build the habit of never asking a partner to resend until you have checked both.

Extension points to use rather than patch: the publishable config (endpoint registration, model swapping if supported), the job contract (your handler is where all provider-specific logic lives), and the stored record (replay/backfill). If you need retries with backoff, dead-letter handling, rate limiting per destination, or delivery analytics, check the source and expect to build them — a package described as "simple" from 2023 almost certainly punts on exactly those. Verify rather than assume, and write down what you found.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings