symfony/webhook
Part of the Symfony ecosystem, symfony/webhook eases both sending and consuming webhooks in PHP applications. It provides building blocks for dispatching outgoing webhooks and validating incoming ones, integrating cleanly with Symfony's event and HTTP components.
Architecture fit. symfony/webhook is a small, framework-agnostic component that solves two distinct problems: inbound (a RequestParserInterface contract that turns a raw Request into one or more RemoteEvent value objects, plus signature-verification helpers) and outbound (a ClientInterface/TransportInterface + AbstractRequest for describing and dispatching a payload). Critically for us, the inbound half is genuinely Laravel-compatible with zero bridging code, because Illuminate\Http\Request extends Symfony\Component\HttpFoundation\Request. The outbound half has no Laravel-native entry point — in Symfony it is normally consumed through the Notifier's webhook transport — so adopting it in Laravel means writing our own transport wiring.
What we actually get vs. what the description claims. The vendor/description text advertises "retries, exponential backoff, idempotency." The component does not implement delivery guarantees; that is delegated to Messenger/queues/HttpClient in Symfony. For us this is the single most important scoping correction: adopt this for parsing, normalization, and signature verification, and budget delivery reliability as separate work on Laravel's queue. Sold internally as "webhook delivery infrastructure," it will underdeliver.
What changed in v8.1.7 (re-reviewed; published 2026-09-15). A single bug fix: a malformed request body is now rejected with 406 Not Acceptable instead of 500 Internal Server Error (#65772, backported across the 8.1.x patch line from 8.1.1 → 8.1.7). There are no API changes, no new features, no deprecations, and no breaking changes — RequestParserInterface, RemoteEvent, and the outbound contracts are untouched. Our architectural conclusion is unchanged. (Note: our earlier record carried the wrong publication date for this tag; the diff v8.1.1...v8.1.7 is still the same single commit, so nothing material follows from the correction beyond cadence framing below.)
The update is nonetheless worth recording for two operational reasons. First, it is a status-code correctness fix: a 500 on garbage input is a false signal of our instability, and providers that retry on 5xx will retry a payload that can never succeed. Reclassifying to 406 stops retry storms and separates "malformed/malicious input" from "our service is broken" in both metrics and alert routing. Second, and more important for us, it exposes a branch-tracking question: the fix is published on the 8.1 line, and v8.x requires PHP ≥ 8.4, while our current environments are on PHP 8.2/8.3 and therefore pinned to ^7.4. We must confirm whether the fix was backported to 7.4; if not, we are knowingly carrying the 500 behavior until our PHP floor moves.
Integration feasibility. Moderate-low effort, but entirely ours to build: one service provider binding parsers into the container, one controller/route (CSRF-exempt, throttled), a mapping from RemoteEvent to a Laravel event, and one parser class per provider. Realistic estimate: 3–5 dev-days for the first provider end-to-end including conformance tests, then ~0.5–1 day per additional provider. The component ships abstract PHPUnit test cases for parsers, which is a genuine quality lever — signature bugs fail silently, so a conformance kit is worth more than the runtime code. A single-line bug fix spanning six patch releases (v8.1.1 → v8.1.7) is also a useful signal in itself: the inbound code path is stable and low-churn, not a moving target we need to babysit.
Technical risk. Intrinsic risk is low: MIT, small surface (~a dozen classes), an active and predictable release train (6.4 LTS → 7.x → 8.x), and versioned changelogs. Adoption risk is the real concern: 0 Packagist dependents on this split repo and 56 stars mean essentially no Laravel community signal, no Laravel-oriented documentation, and no blog/StackOverflow corpus. The 0-dependents figure is misleading (most consumers install via symfony/symfony/FrameworkBundle, so the code is battle-tested) — but Laravel-side we are first movers and own all the glue permanently. Note also the version cliff: v8.x declares PHP ≥ 8.4, so on Laravel 11/12 with PHP 8.2/8.3 we pin ^7.4 — which as of v8.1.7 also means we are one branch behind on correctness fixes, not just on features. That is the cost of the PHP floor, and it is small but real.
Key questions to resolve before committing.
symfony/symfony, not this repo?Stack fit. Clean. The only hard contract is HttpFoundation, which Laravel already uses. There is no dispatcher conflict if we treat the component as a pure parser layer and re-emit results on Laravel's event bus (RemoteEvent → our own WebhookReceived event). Avoid pulling in Symfony's WebhookController/FrameworkBundle glue; write a ~40-line Laravel controller instead — which also means the 406-vs-500 decision is partly our controller's to make (see below).
Compatibility checks first.
composer why-not symfony/webhook ^7.4 against our locked symfony/http-foundation and symfony/http-kernel — Laravel pins these transitively, and adding the component can force minor bumps.^8.0 constraint sneak in via a transitive requirement (PHP 8.4 only).Migration path — strangler, not rewrite. Do not touch working endpoints. Inventory every existing inbound webhook and audit the hand-rolled HMAC code; that audit is arguably the highest-value output of this whole exercise, since it usually uncovers endpoints verifying signatures against re-encoded JSON or skipping verification entirely in non-prod. Then introduce the adapter and route only new providers through it.
Sequencing.
RemoteEvent output against current processing. Gate on zero unexplained diffs.Http::withHeaders, which we need anyway for backoff and dead-lettering.Testing. Adopt the component's abstract parser test cases as a required base class for every parser, plus fixture replay of captured real payloads (valid, tampered body, stale timestamp, unknown event type). This makes signature regressions loud instead of silent. Add malformed-body fixtures explicitly — truncated JSON, wrong content type, oversized body, non-UTF-8 bytes — asserting the 406/400 path, since that is the one behavior that has actually changed under us.
Maintenance. Component maintenance is near-zero and rides Symfony's predictable release train (6.4 LTS → 7.x → 8.x), which is a materially better upgrade story than an unknown vendor's. The v8.1.7 patch is a good example of the shape of change we should expect: rare, surgical, behavior-correcting rather than API-moving. Our maintenance is the adapters and the provider parsers — budget roughly 0.5 engineer-weeks per quarter for parser upkeep, secret rotation, schema drift from providers, and a periodic check that fixes landing on the 8.x branch have equivalents on our pinned branch.
Support. Issues belong to symfony/symfony, not this repo (it is a read-only subtree split), and a niche component may sit for weeks. No commercial support. Practical implication: we must be willing to vendor-patch or open PRs, and we should not adopt it for a revenue-critical path without one engineer able to read the component source.
Scaling. Inbound is stateless and cheap; the bottleneck is invariably downstream processing, so pair it with queued jobs and respond 2xx fast. Outbound via the component is synchronous — one slow customer endpoint blocks the request — so we must wrap it in jobs, at which point we are building the delivery layer ourselves and the outbound value proposition largely evaporates.
Failure modes to instrument from day one.
Ramp-up. One to two days for a Laravel developer to be productive: the abstraction is thin, and "learning the package" is mostly "learning webhooks correctly," which is a durable skill. The friction is nomenclature (RemoteEvent, RequestParserInterface) and the absent Laravel docs — mitigated by one in-repo reference example. Overall this remains a conditional adopt: inbound-only as a parsing and signature-verification layer behind our own adapter, proven on one provider via shadow mode; defer the outbound half until we have a delivery layer to justify it.
How can I help you explore Laravel packages today?