NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
ThreatDefence
Forged inbound requestsHMAC-SHA256 (or RFC 9421) over the id, timestamp and raw body, verified through Sentinel in constant time over every valid secret; at most 8 entries and 4 KB per header.
Replayed captures5-minute tolerance, the signed delivery id deduplicated for 7 days (+ a unique index), a body-digest guard for unsigned ids, RFC 9421 nonces recorded atomically with the stored delivery.
Splitting one message into manyA signed id wins over any Idempotency-Key; the same id with another body is refused.
Endpoint enumerationUnknown, deleted and disabled keys answer the same 404; keys carry about 190 bits; unknown keys fire no event.
Resource exhaustion on the inbound route413 and 415 before any MAC, a rejection guard per endpoint and IP, a per-endpoint delivery limit, processing only on the queue — and the web server’s body limit for the first read.
A receiver answering with an endless bodyRead only up to outbound.max_response_bytes with the guard on and off, within the 10 s budget; response_truncated recorded.
Untrusted payloadsJSON to arrays only, depth ≤ 64; no unserialize, eval or extract; a sanitizer before storage.
Secret disclosureEncrypted at rest, hidden, shown once, redacted DTOs, #[SensitiveParameter], never in events, logs, resources, about or webhooks:endpoints; wiped on delete.
SSRF and DNS rebindingSafe Fetch’s guard on save and on every hop: https only, every resolved address public, app hosts blocked, the connection pinned to the vetted addresses, no proxy, redirects off or re-vetted.
Header injection and spoofingCustom header names and values validated; framing, signature and proxy headers never settable.
Retry stormsExponential backoff with jitter, Retry-After honoured, a per-endpoint throttle, auto-disable.

Not defended

  • Row-swapping by a database writer — the encrypted cast is not bound to its row, so someone who can write the database can copy one endpoint’s secret ciphertext to another.
  • A compromised sender or receiver — whoever holds the secret can sign.
  • Clock skew — senders and receivers need NTP; the tolerance is configurable per endpoint.
  • A host-wide egress proxy — the guard bypasses environment proxies; with WEBHOOKS_SSRF_GUARD=off there is no destination protection at all (the response cap still holds).
  • Shared client IPs — behind a load balancer or a CDN without trusted proxies, a stranger’s bad requests can rate-limit the real sender for a minute.
  • Queued WebhookReceived listeners — they store the sanitized payload in the queue unencrypted.
  • Ordering — none between deliveries to one endpoint; receivers order by the envelope’s timestamp.
  • The per-endpoint outbound throttle on the in-memory store holds per process only.

Exceptions

In RoundlyConsulting\Webhooks\Exceptions, extending WebhooksException (a RuntimeException), with translated messages. Configuration values that do not read throw the toolkit’s InvalidConfigurationException:

ExceptionThrown when
EndpointNotFoundExceptionfind() misses; a scoped handle is given another owner’s endpoint or delivery.
EndpointNotAcceptingExceptionsend(), ping(), redeliver() or redeliverFailed() to a disabled endpoint, send() to an unsubscribed one — reason(): disabled or not_subscribed.
InvalidEndpointExceptionA bad URL, custom header, subscription list, imported secret or option.
InvalidEventTypeExceptionA malformed event type, or one outside outbound.event_types.
PayloadTooLargeExceptionAn encoded outbound body over outbound.max_payload_bytes.
PayloadPrunedExceptionReplay or redeliver of a delivery whose body retention removed.
DeliveryInProgressExceptionReplay of a delivery being processed; redeliver of one still pending, sending or retrying.
DestinationBlockedExceptionThe SSRF guard refuses an outbound URL on save — reason(), blockedBy(), unresolved().
WebhookSignatureExceptionA scheme refuses a request — reason(): RejectionReason; because() for your own schemes.
UnknownSchemeException · UnknownHandlerExceptionAn unregistered name or alias, or a class that is not a scheme or handler.
SecretRotationExceptionA grace in the past or longer than secrets.max_grace_seconds.
SigningUnavailableExceptionInside an attempt, for an endpoint whose scheme cannot sign — caught: the delivery fails with signing.
PayloadSanitizerExceptionThe sanitizer threw (the pipeline answers 500 sanitizer_failed).
InvalidWebhooksConfigurationExceptionA class-valued config key names no implementation; an owner whose key does not fit webhooks.key_type.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.