---
title: "Payloads and retention — Webhooks for Laravel | Roundly"
description: "Inbound payloads as untrusted input, the sanitizer and how to replace it, encryption at rest, and retention with webhooks:prune and a dry run."
url: https://roundly-consulting.com/open-source/docs/webhooks-for-laravel/payloads
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)

# Payloads and retention

## Untrusted input

An inbound payload is data a stranger with the URL — and, for a verified delivery, the secret — chose. It is decoded to arrays only, never objects, depth ≤ 64, big integers as strings, and the package never unserialize()s, eval()s or extract()s anything. In your handler, validate before use, check any value used as a class name, file path, URL or query fragment against an allowlist, and never pass it to unserialize().

## The sanitizer

Contracts\\PayloadSanitizer::sanitize(array $payload, SanitizeContext $context): array runs on an inbound payload before it is stored, handed to the event and the handler, or rendered. The default, Support\\KeyRedactingSanitizer, replaces the value of every key in sanitizer.redact\_keys (case-insensitive, at any depth up to 64) with sanitizer.replacement. Swap it with sanitizer.class:

```php
use RoundlyConsulting\Webhooks\Contracts\PayloadSanitizer;
use RoundlyConsulting\Webhooks\DataTransferObjects\SanitizeContext;

final class DropCardData implements PayloadSanitizer
{
    public function sanitize(array $payload, SanitizeContext $context): array
    {
        // $context->direction (Direction::Inbound), $context->endpoint, $context->eventType
        unset($payload['data']['card']);

        return $payload;
    }
}

// config/webhooks.php
'sanitizer' => ['class' => DropCardData::class, /* … */],
```

A sanitizer that throws refuses the delivery with 500 sanitizer\_failed (reported): nothing unsanitized is ever stored, and the delivery id is released, so the sender’s retry is accepted once the sanitizer works — for RFC 9421 the unchanged resend too. Outbound bodies are sent unchanged; they are your own data.

## At rest

- Inbound: the sanitized, decoded payload (never the raw body), payload\_bytes and payload\_digest (SHA-256 of the raw body), and the headers in inbound.store\_headers — signature, credential and cookie headers are never stored, whatever the list says.
- Outbound: the exact body sent on every attempt.
- With payloads.encrypt (on) both are encrypted with the app key; each row records how it was written, so switching the setting never makes old rows unreadable.

## Retention

| What | When |
| --- | --- |
| Inbound payloads and outbound bodies of finished deliveries, attempt response excerpts → cleared (payload\_pruned\_at) | Older than payloads.retention\_days (30) |
| Inbound deliveries, finished outbound deliveries and their attempts → deleted for good | Older than payloads.delivery\_retention\_days (90; null = keep) |
| Rotated-out secrets past their grace → cleared | Always |
| Soft-deleted endpoints no delivery points at any more → deleted for good | Deleted longer than delivery\_retention\_days ago (null = keep) |

```php
$result = Webhooks::prune(new PruneOptions(dryRun: true));   // counts only, changes nothing

$result->total();
$result->inboundPayloads;     // also: outboundBodies, attemptBodies, inboundDeliveries, outboundDeliveries, attempts, expiredSecrets, endpoints
```

PruneOptions(bool $dryRun = false, ?CarbonInterface $now = null) — now moves the cut-off, for a backfill or a test. The prune runs in chunks and returns a PruneResult with counts per kind. A pruned delivery cannot be replayed or redelivered (PayloadPrunedException). The provider schedules it daily (schedule.prune); from the console:

```bash
php artisan webhooks:prune --dry-run
```

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