---
title: "Acknowledging changes — Sentinel for Laravel | Roundly"
description: "Accept an out-of-band change with a reason and an actor, gate who may do it, unseal deliberately and load a tampered row for review."
url: https://roundly-consulting.com/open-source/docs/sentinel-for-laravel/acknowledging
language: en
---

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

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

# Acknowledging changes

A model changed outside the application stays refused until someone with a reason accepts the change. Accepting it is an acknowledgement: a re-seal that records who, why, what changed and what the status was — inside the MAC’d ledger.

```php
$result = Sentinel::for($invoice)->by($admin)->because('INC-88: refund fixed by the DBA')->acknowledge();

$result->acknowledged;   // false when the model was intact (nothing written)
$result->before;         // the VerificationResult it replaced

$invoice->acknowledgeTampering('INC-88: refund fixed by the DBA', $admin);
```

AcknowledgementResult has acknowledged (false when the model was intact — nothing is written), before (the VerificationResult it replaced) and seal (the new SealResult).

## Rules

- A reason is always required, trimmed, 1–sealing.reason\_max\_length characters (AcknowledgementDeniedException::reason / reasonTooLong).
- An actor is required outside the console (actorRequired). Without by(), the logged-in user is used; in the console without one, the actor is recorded as the system.
- The policy decides: by default the Gate ability in acknowledgement.ability (when set), called with the model and the seal name. Bind Contracts\\AcknowledgementPolicy to replace it — for example with a two-person approval.
- Verify-on-retrieve is suspended for the call, so a tampered model can be loaded.
- The new ledger entry (event acknowledged) carries the previous status, the changed attribute names, the actor and the reason; TamperAcknowledged fires after commit. Editing a stored reason or actor afterwards breaks the entry’s MAC (EntryInvalid).

```php
use Illuminate\Support\Facades\Gate;

Gate::define('acknowledge-tampering', fn (User $user, Model $model, string $seal): bool => $user->isAdmin());
// SENTINEL_ACKNOWLEDGE_ABILITY=acknowledge-tampering
```

## Flat forms

```php
Sentinel::acknowledge($invoice, 'INC-88: refund fixed by the DBA', $admin, 'financial');   // AcknowledgementResult
Sentinel::unseal($invoice, 'GDPR erasure #12', $admin, 'identity');    // bool: a seal row existed
Sentinel::ledgerHistory($invoice, 'financial', limit: 20);             // list<LedgerRecord>, newest first (1–1000)
Sentinel::currentSeal($invoice);                                       // ?SealRecord — the default seal, unverified
```

## Unsealing

unseal($reason) deletes the seal row and appends an unsealed tombstone (once — a repeated unseal writes nothing) recording the reason, the actor and the status the seal had; SealRemoved fires after commit. Removing a seal is as strong as accepting a change, so it runs the same reason, actor and policy checks. Afterwards the seal verifies Unsealed (lenient) or Missing(unsealed) (strict); a strict seal comes back through acknowledge(), never seal().

```php
use RoundlyConsulting\Sentinel\Actions\Seals\UnsealModelAction;
use RoundlyConsulting\Sentinel\DataTransferObjects\UnsealRequest;

Sentinel::for($invoice, 'identity')->by($admin)->unseal('GDPR erasure #12');
Sentinel::unseal($invoice, 'GDPR erasure #12', $admin, 'identity');
app(UnsealModelAction::class)->execute(new UnsealRequest($invoice, 'identity', 'GDPR erasure #12', $admin));
```

## Loading a tampered model

A seal that verifies on retrieve with Reaction::Throw refuses to load the tampered row — including in route-model binding, before the acknowledging controller runs. Load it for the acknowledgement screen with find() / findOrFail(), which suspend verify-on-retrieve for that one query (global scopes stay; findOrFail() is a 404 in a route):

```php
Route::bind('tamperedInvoice', fn (string $id) => Sentinel::model(Invoice::class)->findOrFail($id));
Route::post('/admin/invoices/{tamperedInvoice}/acknowledge', AcknowledgeInvoice::class);

$invoice = Sentinel::withoutVerification(fn () => Invoice::query()->find($id));   // any query, verify-on-retrieve off
```

Verification stays on everywhere else; acknowledge() itself suspends it only inside its own call. Both work under the fake.

## 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/sentinel-for-laravel.md)
