---
title: "The Sentinel facade — Sentinel for Laravel | Roundly"
description: "The Sentinel facade — the for($model) handle, flat seal verbs, model-wide operations and the keys, ledger, idempotency, nonces and signatures accessors."
url: https://roundly-consulting.com/open-source/docs/sentinel-for-laravel/facade
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)

# The Sentinel facade

Everything goes through one facade, RoundlyConsulting\\Sentinel\\Facades\\Sentinel — import it, there is no global alias. for($model) returns a handle bound to one seal, flat verbs act on the model you hand them, model($class) works on every row of a model, and five sub-accessors group the rest: keys(), ledger(), idempotency(), nonces() and signatures().

```php
use RoundlyConsulting\Sentinel\Enums\Algorithm;
use RoundlyConsulting\Sentinel\Facades\Sentinel;

// One model: a handle bound to a seal (the default one when unnamed)
Sentinel::for($invoice)->verify();
Sentinel::for($invoice, 'identity')->verify();
Sentinel::for($invoice)->by($admin)->because('INC-88: refund fixed by the DBA')->acknowledge();

// Flat verbs act on the model you hand them
Sentinel::seal($invoice, reason: 'Recomputed lines');
Sentinel::verify($invoice, 'financial');
Sentinel::verifyAll($invoice)->allIntact();

// Every row of a model
Sentinel::model(Invoice::class)->scan();

// Sub-accessors
Sentinel::keys()->ring('http')->import('acme-2026-10', Algorithm::Ed25519, $partnerPublicKeyPem, owner: $partner);
Sentinel::ledger()->checkpoint();
Sentinel::idempotency()->run("charge:{$order->id}", scope: 'billing', callback: fn () => $gateway->charge($order));
Sentinel::nonces()->issue('password-reset', ttl: 900, subject: $user);
Sentinel::signatures()->current($request);
```

## Model seals

A seal name the model does not declare throws SealingMisconfiguredException::unknownSeal (the message lists the declared seals); a null seal is the model’s default. An unsaved model throws SealingFailedException::notPersisted from seal(), verify(), verifyAll() and isIntact() — there is nothing to seal or verify yet.

| Method | Returns | Does |
| --- | --- | --- |
| `for(Model $model, ?string $seal = null)` | `SealHandle` | A handle bound to one seal (the default one when unnamed). |
| `model(string $class)` | `ModelSeals` | Class-level operations over every row of a model. |
| `sealables()` | `list<class-string<Model>>` | sentinel.models first, then every class with seal rows or ledger entries. |
| `check()` | `HealthReport` | The installation health check. |
| `seal(Model $model, ?string $seal = null, ?string $reason = null, ?Model $actor = null)` | `SealResult` | Explicit seal; refused on a tampered model. |
| `verify(Model $model, ?string $seal = null)` | `VerificationResult` | Verify one seal. |
| `verifyOrFail(Model $model, ?string $seal = null)` | `VerificationResult` | Verify or throw TamperedModelException. |
| `verifyAll(Model $model)` | `VerificationReport` | Every declared seal of one model. |
| `verifyMany(iterable $models, ?string $seal = null)` | `VerificationReport` | Many models; a null seal = every seal of each. |
| `isIntact(Model $model)` | `bool` | True when every seal is intact. |
| `acknowledge(Model $model, string $reason, ?Model $actor = null, ?string $seal = null)` | `AcknowledgementResult` | Accept an out-of-band change with a reason. |
| `unseal(Model $model, string $reason, ?Model $actor = null, ?string $seal = null)` | `bool` | Remove a seal deliberately; returns whether a seal row existed. |
| `ledgerHistory(Model $model, ?string $seal = null, int $limit = 50)` | `list<LedgerRecord>` | Ledger records, newest first (limit 1–1000). |
| `currentSeal(Model $model, ?string $seal = null)` | `?SealRecord` | The stored seal row, unverified (sealedAt is null when the stored sealed\_at is corrupt). |
| `scan(ScanOptions $options)` | `ScanReport` | Chunked verification of every row. |
| `reseal(ResealOptions $options)` | `ResealReport` | Re-seal intact rows with the current key or definition. |
| `resealWhere(ResealWhereRequest $request)` | `ResealReport` | Acknowledge every selected row. |
| `updateAndReseal(UpdateAndResealRequest $request)` | `ResealReport` | A verified mass update. |
| `sealMissing(BaselineOptions $options)` | `ResealReport` | Baseline rows that never had a seal. |
| `withoutSealing(Closure $callback, string $reason)` | `mixed` | Run the callback with sealing suspended. |
| `withoutVerification(Closure $callback)` | `mixed` | Run the callback with verify-on-retrieve suspended. |

```php
$result  = Sentinel::seal($invoice, reason: 'Recomputed lines');           // SealResult
$result  = Sentinel::verify($invoice);                                       // the default seal
$result  = Sentinel::verifyOrFail($invoice, 'financial');
$report  = Sentinel::verifyAll($invoice);                                    // every seal
$report  = Sentinel::verifyMany(Invoice::query()->latest()->limit(50)->get(), 'financial');
$ok      = Sentinel::isIntact($invoice);
$removed = Sentinel::unseal($invoice, 'GDPR erasure #12', $admin, 'identity');
$history = Sentinel::ledgerHistory($invoice, 'financial', limit: 20);       // list<LedgerRecord>
$stored  = Sentinel::currentSeal($invoice);                                  // ?SealRecord
$classes = Sentinel::sealables();                                            // [Invoice::class, …]
```

## Keys

verifyMac() and mac() work only on a ring of their own — see MACs for your own services.

| Method | Returns | Does |
| --- | --- | --- |
| `keys()` | `KeysAccessor` | Rings, key inventory and ring handles. |
| `generateKey(GenerateKeyRequest $request)` | `GeneratedKey` | Generate a key (database by default, or env lines). |
| `importKey(ImportKeyRequest $request)` | `KeyInfo` | Import a partner’s (or your own) key. |
| `rotateKey(RotateKeyRequest $request)` | `RotationResult` | Rotate a ring’s signing key. |
| `revokeKey(RevokeKeyRequest $request)` | `KeyInfo` | Revoke a database key (reason required). |
| `retireKey(string $ring, string $keyId)` | `KeyInfo` | End a database key’s verification period now. |
| `listKeys(?string $ring = null)` | `list<KeyInfo>` | Null = every ring; never material. |
| `findKey(string $ring, string $keyId)` | `?KeyInfo` | One key in one ring. |
| `currentKey(?string $ring = null)` | `KeyInfo` | The signing key, or NoSigningKeyException. |
| `verifyMac(string $ring, string $keyId, string $message, string $mac)` | `KeyInfo` | Verify a MAC over arbitrary bytes: the key that vouches, or MacVerificationException. |
| `mac(string $ring, string $message)` | `IssuedMac` | MAC a message with the ring’s current signing key. |
| `extend(string $driver, Closure $factory)` | `static` | Register a key-store driver. |

## Ledger

| Method | Returns | Does |
| --- | --- | --- |
| `ledger()` | `LedgerAccessor` | Checkpoints, verification, history, head, anchors. |
| `checkpoint(?CheckpointOptions $options = null)` | `?CheckpointResult` | One batch; null = nothing pending. |
| `verifyLedger(?LedgerVerifyOptions $options = null)` | `LedgerReport` | Checkpoints, anchors, pending entries, entity heads. |
| `ledgerHead(?string $connection = null)` | `?CheckpointRecord` | The newest checkpoint, unverified. |
| `anchors()` | `list<string>` | The configured anchor names. |
| `extendAnchor(string $driver, Closure $factory)` | `static` | Register an anchor driver (a write-only one implements Contracts\\WriteOnlyAnchor). |

## Idempotency, nonces and pruning

| Method | Returns | Does |
| --- | --- | --- |
| `idempotency()` | `IdempotencyAccessor` | run() and forget() for jobs, commands and webhooks. |
| `runIdempotent(IdempotentCall $call)` | `IdempotentResult` | Run a callback at most once per key and scope. |
| `forgetIdempotencyKey(string $key, string $scope)` | `bool` | Whether the key existed. |
| `nonces()` | `NoncesAccessor` | Issue, consume, single-use URLs. |
| `issueNonce(IssueNonceRequest $request)` | `IssuedNonce` | A purpose-bound, single-use token. |
| `consumeNonce(ConsumeNonceRequest $request)` | `bool` | True exactly once. |
| `signedRoute(SignedRouteRequest $request)` | `string` | A single-use signed URL. |
| `prune(?PruneOptions $options = null)` | `PruneResult` | Delete expired idempotency keys and nonces. |

## HTTP message signatures

| Method | Returns | Does |
| --- | --- | --- |
| `signatures()` | `SignaturesAccessor` | Sign, verify, current, owner, contentDigest. |
| `signRequest(RequestInterface $request, string $keyId, ?SigningOptions $options = null)` | `RequestInterface` | Sign any PSR-7 request. |
| `verifyRequestSignature(Request $request, ?string $profile = null)` | `VerifiedSignature` | Or HttpSignatureException (401). |
| `verifyResponseSignature(ResponseInterface\|ClientResponse $response, ?string $profile = null)` | `VerifiedSignature` | A signed response (PSR-7 or Laravel client). |
| `verifiedSignature(Request $request)` | `?VerifiedSignature` | What sentinel.signed verified on this request. |
| `signatureOwner(Request\|VerifiedSignature $from)` | `?Model` | The owner of the signing key. |

## Handles and sub-accessors

All are final readonly objects returned by the manager; each method calls one manager method. A ring handle refuses a key id of another ring (UnknownKeyException), and a ModelSeals query of another model class is refused (SealingMisconfiguredException::queryModelMismatch). ModelSeals methods take the same defaults as the option objects (chunk 500, checkLedger true, maxFindings 1000); checkSchema and upgradeFormat exist only on ScanOptions / ResealOptions and the commands.

| Entry | Class | Methods |
| --- | --- | --- |
| `Sentinel::for($model, ?$seal)` | `SealHandle` | `by(?Model $actor), because(string $reason), name(), definition(), seal(), verify(), verifyOrFail(), isIntact() (this seal only), acknowledge(?string $reason = null), unseal(?string $reason = null), current(), history(int $limit = 50)` |
| `Sentinel::model(Invoice::class)` | `ModelSeals` | `definition(?string $seal = null), seals(), scan(…), reseal(…), resealWhere(…), updateAndReseal(…), sealMissing(…), find($id), findOrFail($id), unsealedQuery(?string $seal = null)` |
| `Sentinel::keys()` | `Accessors\KeysAccessor` | `ring(?string $ring = null), all(), rings()` |
| `Sentinel::keys()->ring($ring)` | `Accessors\KeyRingHandle` | `name(), current(), find(string $keyId), all(), generate(…), import(…), rotate(?Algorithm $algorithm = null, ?CarbonInterface $activatesAt = null), revoke(string $keyId, string $reason, ?Model $actor = null), retire(string $keyId), verifyMac(string $keyId, string $message, string $mac), mac(string $message)` |
| `Sentinel::ledger()` | `Accessors\LedgerAccessor` | `checkpoint(?string $connection = null), verify(?string $connection = null, bool $entities = true, int $chunk = 1000), history(Model $model, ?string $seal = null, int $limit = 50), head(?string $connection = null), anchors()` |
| `Sentinel::idempotency()` | `Accessors\IdempotencyAccessor` | `run(string $key, string $scope, Closure $callback, ?string $fingerprint = null, ?int $ttl = null, ?int $lease = null), forget(string $key, string $scope)` |
| `Sentinel::nonces()` | `Accessors\NoncesAccessor` | `issue(string $purpose, ?int $ttl = null, ?Model $subject = null), consume(…), consumeOrFail(…), signedRoute(string $name, array $parameters = [], ?int $ttl = null)` |
| `Sentinel::signatures()` | `Accessors\SignaturesAccessor` | `sign(…), verify(Request $request, ?string $profile = null), verifyResponse(…), current(Request $request), owner(Request\|VerifiedSignature $from), contentDigest(string $body, DigestAlgorithm $algorithm = DigestAlgorithm::Sha256)` |

## The model trait shorthand

The HasSeals trait, its query scopes and the verifySeals() collection macro call the same manager — so Sentinel::fake() sees them too:

```php
$invoice->seal('financial');                         // SealResult
$invoice->verifySeal('identity');                    // VerificationResult
$invoice->verifySealOrFail();                        // TamperedModelException when not intact
$invoice->isIntact();                                // every seal
$invoice->acknowledgeTampering('INC-88: refund fixed by the DBA', $admin);

Invoice::query()->whereSealed()->count();
Invoice::query()->whereNotSealed('identity')->get();
Invoice::query()->withSeals()->get()->verifySeals();
```

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