---
title: "Configuration — Webhooks for Laravel | Roundly"
description: "Every key of config/webhooks.php — keys and models, queues, the inbound pipeline, outbound delivery, SSRF, secrets, schemes, payloads and the schedule."
url: https://roundly-consulting.com/open-source/docs/webhooks-for-laravel/configuration
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Webhooks for Laravel](https://roundly-consulting.com/open-source/docs/webhooks-for-laravel.md)

# Configuration

The package works with zero configuration. Publish config/webhooks.php with --tag=webhooks-config to change it (shown without its comments):

```php
use RoundlyConsulting\Webhooks\Http\Resources\WebhookOwnerResource;
use RoundlyConsulting\Webhooks\Models\InboundDelivery;
use RoundlyConsulting\Webhooks\Models\InboundEndpoint;
use RoundlyConsulting\Webhooks\Models\OutboundAttempt;
use RoundlyConsulting\Webhooks\Models\OutboundDelivery;
use RoundlyConsulting\Webhooks\Models\OutboundEndpoint;
use RoundlyConsulting\Webhooks\Schemes\DefaultScheme;
use RoundlyConsulting\Webhooks\Schemes\Rfc9421Scheme;
use RoundlyConsulting\Webhooks\Support\KeyRedactingSanitizer;

return [

    'key_type' => env('WEBHOOKS_KEY_TYPE', 'bigint'),
    'primary_key_type' => env('WEBHOOKS_PRIMARY_KEY_TYPE', 'bigint'),

    'models' => [
        'inbound_endpoint' => InboundEndpoint::class,
        'inbound_delivery' => InboundDelivery::class,
        'outbound_endpoint' => OutboundEndpoint::class,
        'outbound_delivery' => OutboundDelivery::class,
        'outbound_attempt' => OutboundAttempt::class,
    ],

    'tables' => [
        'inbound_endpoints' => 'webhook_inbound_endpoints',
        'inbound_deliveries' => 'webhook_inbound_deliveries',
        'outbound_endpoints' => 'webhook_outbound_endpoints',
        'outbound_deliveries' => 'webhook_outbound_deliveries',
        'outbound_attempts' => 'webhook_outbound_attempts',
    ],

    'resources' => [
        'owner' => WebhookOwnerResource::class,
    ],

    'queue' => [
        'connection' => env('WEBHOOKS_QUEUE_CONNECTION'),
        'queue' => env('WEBHOOKS_QUEUE_NAME'),
        'inbound' => env('WEBHOOKS_INBOUND_QUEUE'),
        'outbound' => env('WEBHOOKS_OUTBOUND_QUEUE'),
        'max_delay' => 900,
    ],

    'inbound' => [
        'route' => [
            // POST {prefix}/{key}. Off: mount your own route calling Webhooks::inbound()->receive().
            'enabled' => env('WEBHOOKS_INBOUND_ROUTE', true),
            'prefix' => 'webhooks',
            'domain' => null,
            // Never `web`: a sender has no session or CSRF token.
            'middleware' => [],
            // The base the endpoint URL is built on (e.g. behind a proxy); blank = route().
            'url_base' => env('WEBHOOKS_INBOUND_URL'),
        ],
        'tolerance' => 300,
        'max_payload_bytes' => 262144,
        'max_payload_bytes_ceiling' => 1048576,
        'content_types' => ['application/json', 'application/*+json'],
        'on_duplicate' => 'accept',
        'accept_idempotency_header' => true,
        'dedupe_ttl' => 604800,
        'rate_limit' => [
            'per_minute' => 600,
            'rejected_per_minute' => 60,
            'cache_store' => null,
        ],
        'store_headers' => ['content-type', 'user-agent', 'webhook-id', 'webhook-timestamp'],
        'requeue_after' => 60,
        'handler' => [
            'tries' => 3,
            'backoff' => [10, 60, 300],
            'timeout' => 60,
        ],
    ],

    'outbound' => [
        'allow_http' => env('WEBHOOKS_ALLOW_HTTP', false),
        'verify_on_save' => true,
        'envelope' => true,
        'event_types' => [],
        'max_payload_bytes' => 262144,
        'max_response_bytes' => 4096,
        'timeouts' => [
            'connect' => 5,
            'total' => 10,
        ],
        'max_attempts' => 8,
        'retry' => [
            // delay(n) = min(base * multiplier^(n-1), cap) seconds, then ± jitter_permille.
            'base' => 60,
            'multiplier' => 4,
            'cap' => 32400,
            'jitter_permille' => 100,
            'max_retry_after' => 86400,
            'schedule' => null,
        ],
        'rate_limit' => [
            'per_minute' => 60,
        ],
        'requeue_after' => 120,
        'sweep_batch' => 1000,
        'disable_after' => 5,
        'disable_on_gone' => true,
        'follow_redirects' => false,
        'max_redirects' => 3,
        'headers' => [
            'allowed_overrides' => [],
        ],
        // The User-Agent your receivers see. Blank = "{app.name} Webhooks".
        'user_agent' => env('WEBHOOKS_USER_AGENT'),
    ],

    'ssrf' => [
        // Off only behind a filtering egress proxy you trust (webhooks:doctor warns).
        'enabled' => env('WEBHOOKS_SSRF_GUARD', true),
        'block_app_hosts' => true,
        'app_hosts' => [],
        'allowed_hosts' => [],
        'blocked_hosts' => [],
        'allowed_ranges' => [],
        'blocked_ranges' => [],
        'allowed_ports' => null,
    ],

    'secrets' => [
        'bytes' => 32,
        'grace_seconds' => 86400,
        'max_grace_seconds' => 604800,
    ],

    'schemes' => [
        'default' => DefaultScheme::class,
        'rfc9421' => Rfc9421Scheme::class,
    ],

    'rfc9421' => [
        'profile' => 'default',
        'outbound_key_id' => null,
    ],

    'handlers' => [],

    'sanitizer' => [
        'class' => KeyRedactingSanitizer::class,
        'redact_keys' => ['password', 'secret', 'token', 'api_key', 'authorization', 'card_number', 'cvc', 'ssn'],
        'replacement' => '[redacted]',
    ],

    'payloads' => [
        'encrypt' => true,
        'retention_days' => 30,
        'delivery_retention_days' => 90,
    ],

    'schedule' => [
        'enabled' => true,
        'retry' => 'everyMinute',
        'prune' => 'daily',
    ],

];
```

## Strict reading

- Every value is read strictly: a typo or an out-of-range value throws the toolkit’s InvalidConfigurationException naming the key.
- A class-valued key — schemes.\*, handlers.\*, sanitizer.class — that names no instantiable implementation of its contract throws InvalidWebhooksConfigurationException, also naming the key.
- A blank value (KEY=) means not set, and the default applies. Switches accept true/false, 1/0, on/off and yes/no.
- Integer lists (handler.backoff, retry.schedule, ssrf.allowed\_ports) also accept a comma-separated string ("60,300,3600").

## Keys, models and tables

| Key | Env | Default | Purpose |
| --- | --- | --- | --- |
| `key_type` | `WEBHOOKS_KEY_TYPE` | bigint | bigint, uuid or ulid — the key type of the host models that own endpoints (the owner morph columns). Set it before migrating. |
| `primary_key_type` | `WEBHOOKS_PRIMARY_KEY_TYPE` | bigint | bigint, uuid (UUIDv7) or ulid — the package tables’ own ids and foreign keys. Before migrating. Wire ids are ULIDs either way. |
| `models.*` | — | the packaged models | inbound\_endpoint, inbound\_delivery, outbound\_endpoint, outbound\_delivery, outbound\_attempt — a replacement must extend the packaged model. |
| `tables.*` | — | webhook\_inbound\_endpoints, … | The five table names; each must match ^\[A-Za-z\_\]\[A-Za-z0-9\_.\]{0,63}$. |
| `resources.owner` | — | WebhookOwnerResource | How an endpoint’s owner renders in the JSON resources — an instantiable JsonResource subclass. |

## Queue

| Key | Env | Default | Purpose |
| --- | --- | --- | --- |
| `queue.connection` | `WEBHOOKS_QUEUE_CONNECTION` | blank = default | The connection every job of the package uses. |
| `queue.queue` | `WEBHOOKS_QUEUE_NAME` | blank = the connection’s default | The fallback queue for both directions. |
| `queue.inbound` | `WEBHOOKS_INBOUND_QUEUE` | blank = queue.queue | The queue of ProcessInboundDelivery. |
| `queue.outbound` | `WEBHOOKS_OUTBOUND_QUEUE` | blank = queue.queue | The queue of DeliverWebhook. |
| `queue.max_delay` | — | 900 | 1–43 200 s — the longest delayed job; a longer retry waits for the webhooks:retry sweep (SQS cannot delay over 900 s). |

## Inbound

| Key | Env | Default | Purpose |
| --- | --- | --- | --- |
| `inbound.route.enabled` | `WEBHOOKS_INBOUND_ROUTE` | on | The package’s POST {prefix}/{key} route; off → mount your own. |
| `inbound.route.prefix` | — | webhooks | The route prefix (slashes trimmed). |
| `inbound.route.domain` | — | null | A route domain. |
| `inbound.route.middleware` | — | \[\] | Middleware names — never web: a sender has no session or CSRF token. |
| `inbound.route.url_base` | `WEBHOOKS_INBOUND_URL` | null = route() | Base of the endpoint URLs shown to you ({url\_base}/{key}), e.g. behind a proxy. |
| `inbound.tolerance` | — | 300 | 1–3 600 s — how far a signed timestamp may be from now, past or future. |
| `inbound.max_payload_bytes` | — | 262144 | Body size limit (256 KB), 1 – max\_payload\_bytes\_ceiling. |
| `inbound.max_payload_bytes_ceiling` | — | 1048576 | 1 – 67 108 864 — the most a per-endpoint override may allow (1 MB). |
| `inbound.content_types` | — | application/json, application/\*+json | Accepted Content-Type media types (\* allowed in the subtype, parameters ignored). |
| `inbound.on_duplicate` | — | accept | accept answers a repeat 200 {"status":"duplicate"}; conflict answers 409. |
| `inbound.accept_idempotency_header` | — | on | Use Idempotency-Key when the scheme carries no delivery id. |
| `inbound.dedupe_ttl` | — | 604800 | 60 – 2 592 000 s — how long delivery ids (and body digests of unsigned ids) are remembered (7 days). |
| `inbound.rate_limit.per_minute` | — | 600 | Verified deliveries per endpoint per minute. |
| `inbound.rate_limit.rejected_per_minute` | — | 60 | Rejected requests per endpoint and client IP per minute. |
| `inbound.rate_limit.cache_store` | — | null = default store | The limiters’ cache store — must be shared, never array. |
| `inbound.store_headers` | — | content-type, user-agent, webhook-id, webhook-timestamp | Headers stored with a delivery; signature, credential and cookie headers are dropped whatever the list says. |
| `inbound.requeue_after` | — | 60 | A stored delivery not queued after this many seconds is queued by the sweep. |
| `inbound.handler.tries` | — | 3 | 1–100 — ProcessInboundDelivery::$tries. |
| `inbound.handler.backoff` | — | \[10, 60, 300\] | Seconds between the processing job’s tries (a list, or a comma-separated string). |
| `inbound.handler.timeout` | — | 60 | 1–86 400 s — the processing job’s timeout. |

## Outbound

| Key | Env | Default | Purpose |
| --- | --- | --- | --- |
| `outbound.allow_http` | `WEBHOOKS_ALLOW_HTTP` | off | http as well as https — local development only. |
| `outbound.verify_on_save` | — | on | Resolve and vet the URL on create and update; off = every rule but the DNS lookup. |
| `outbound.envelope` | — | on | Wrap payloads in {"type","timestamp","data"}; off = the payload as is. |
| `outbound.event_types` | — | \[\] | Known types; empty = any valid name, non-empty = only these (InvalidEventTypeException). |
| `outbound.max_payload_bytes` | — | 262144 | Encoded body limit (PayloadTooLargeException). |
| `outbound.max_response_bytes` | — | 4096 | 1 – 1 048 576 — how much of a receiver’s answer is read and stored. |
| `outbound.timeouts.connect` | — | 5 | 1–60 s — connect timeout. |
| `outbound.timeouts.total` | — | 10 | connect – 300 s — one budget across every redirect hop; DeliverWebhook::$timeout is this + 20. |
| `outbound.max_attempts` | — | 8 | 1–25 attempts per delivery series. |
| `outbound.retry.base` | — | 60 | delay(n) = min(base · multiplier^(n-1), cap). |
| `outbound.retry.multiplier` | — | 4 | 1–100. |
| `outbound.retry.cap` | — | 32400 | The longest computed delay (9 h). |
| `outbound.retry.jitter_permille` | — | 100 | 0–500 — random spread (± 10 %). |
| `outbound.retry.max_retry_after` | — | 86400 | The longest Retry-After honoured. |
| `outbound.retry.schedule` | — | null | Explicit delays instead of the formula (\[60, 300, 3600\]); past its end the last value repeats. |
| `outbound.rate_limit.per_minute` | — | 60 | Requests per endpoint per minute (an endpoint may override). |
| `outbound.requeue_after` | — | 120 | A due delivery whose job is this late is dispatched again; each further copy waits twice as long (up to an hour). |
| `outbound.sweep_batch` | — | 1000 | 1 – 100 000 — the most outbound jobs one webhooks:retry run dispatches. |
| `outbound.disable_after` | — | 5 | Deliveries that ended failed in a row before the endpoint is disabled. |
| `outbound.disable_on_gone` | — | on | A 410 Gone answer disables the endpoint at once. |
| `outbound.follow_redirects` | — | off | Follow 307/308 (each hop vetted again). |
| `outbound.max_redirects` | — | 3 | 0–10. |
| `outbound.headers.allowed_overrides` | — | \[\] | Reserved custom headers an endpoint may still set: host (only with ssrf.enabled off), authorization. |
| `outbound.user_agent` | `WEBHOOKS_USER_AGENT` | blank = "{app.name} Webhooks" | The User-Agent receivers see (Laravel Webhooks without an app name). |

## Outbound safety (SSRF)

| Key | Env | Default | Purpose |
| --- | --- | --- | --- |
| `ssrf.enabled` | `WEBHOOKS_SSRF_GUARD` | on | The guard; off only behind a filtering egress proxy you trust. |
| `ssrf.block_app_hosts` | — | on | Refuse app.url’s host and ssrf.app\_hosts. |
| `ssrf.app_hosts` | — | \[\] | More names of your own app. |
| `ssrf.allowed_hosts` | — | \[\] | Exact or \*.example.com; non-empty means only these hosts (still resolved and vetted). |
| `ssrf.blocked_hosts` | — | \[\] | Always refused. |
| `ssrf.allowed_ranges` | — | \[\] | IPs or CIDRs exempt from the private-network table (127.0.0.1/32 locally). |
| `ssrf.blocked_ranges` | — | \[\] | IPs or CIDRs always refused — block beats allow. |
| `ssrf.allowed_ports` | — | null = any | A list of ports 1–65 535. |

## Secrets, schemes and handlers

| Key | Env | Default | Purpose |
| --- | --- | --- | --- |
| `secrets.bytes` | — | 32 | 24–64 random bytes in a generated secret (whsec\_ + base64). |
| `secrets.grace_seconds` | — | 86400 | Default grace of a rotated-out secret, 0 – max\_grace\_seconds. |
| `secrets.max_grace_seconds` | — | 604800 | The longest grace rotateSecret() accepts (0 – 31 536 000). |
| `schemes` | — | default ⇒ DefaultScheme, rfc9421 ⇒ Rfc9421Scheme | Name ⇒ a SignatureScheme class; add yours here or with Webhooks::schemes()->extend(). |
| `rfc9421.profile` | — | default | The Sentinel signature profile inbound RFC 9421 endpoints verify against (an endpoint’s scheme\_options.profile wins). |
| `rfc9421.outbound_key_id` | — | null | The Sentinel key outbound RFC 9421 endpoints sign with (an endpoint’s scheme\_options.key\_id wins). |
| `handlers` | — | \[\] | Alias ⇒ an InboundHandler class; closures through Webhooks::handlers()->register(). |

## Payloads, retention and schedule

| Key | Env | Default | Purpose |
| --- | --- | --- | --- |
| `sanitizer.class` | — | KeyRedactingSanitizer | A PayloadSanitizer class run on every inbound payload before it is stored. |
| `sanitizer.redact_keys` | — | password, secret, token, api\_key, authorization, card\_number, cvc, ssn | Key names the default sanitizer redacts (case-insensitive). |
| `sanitizer.replacement` | — | \[redacted\] | The value they get. |
| `payloads.encrypt` | — | on | Encrypt stored payloads and bodies with the app key. |
| `payloads.retention_days` | — | 30 | 1–36 500 — payloads, bodies and response excerpts are cleared after this. |
| `payloads.delivery_retention_days` | — | 90 | 1–36 500 or null = keep — delivery and attempt rows (and deleted endpoints nothing points at) are deleted after this. |
| `schedule.enabled` | — | on | The provider schedules webhooks:retry and webhooks:prune. |
| `schedule.retry` | — | everyMinute | everyMinute, everyTwoMinutes or everyFiveMinutes. |
| `schedule.prune` | — | daily | hourly, daily or weekly. |

## Per-endpoint overrides

Some settings can be overridden per endpoint through the options objects (limits in Managing endpoints):

| Setting | InboundEndpointOptions | OutboundEndpointOptions |
| --- | --- | --- |
| Rate limit per minute | `rateLimitPerMinute` | `rateLimitPerMinute` |
| Body size | `maxPayloadBytes` | — |
| Content types | `contentTypes` | — |
| Timestamp tolerance | `toleranceSeconds` | — |
| Scheme options | `schemeOptions (profile, id_header)` | `schemeOptions (key_id)` |
| Description | — | `description` |

## Environment variables

```bash
WEBHOOKS_KEY_TYPE=bigint
WEBHOOKS_PRIMARY_KEY_TYPE=bigint
WEBHOOKS_QUEUE_CONNECTION=
WEBHOOKS_QUEUE_NAME=
WEBHOOKS_INBOUND_QUEUE=
WEBHOOKS_OUTBOUND_QUEUE=
WEBHOOKS_INBOUND_ROUTE=true
WEBHOOKS_INBOUND_URL=
WEBHOOKS_ALLOW_HTTP=false
WEBHOOKS_USER_AGENT=
WEBHOOKS_SSRF_GUARD=true
```

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/webhooks-for-laravel.md)
