Confirmation
->confirm() only asks clients to show a dialog: a client that skips it still runs the action. When an action or a write must not run unconfirmed, declare a confirmation hook. The endpoint then answers 428 with a signed token, and runs only when the same request comes back with that token.
- Actions declare
confirmation(). - Resources declare
mutateConfirmation(),destroyConfirmation(),restoreConfirmation()andforceDestroyConfirmation(), see Confirming writes.
Asking for a confirmation
confirmation() receives the fields and the number of rows the action would run on. Return a Confirmation to ask, or null to run at once:
use Lomkit\Rest\Actions\ActionFields;
use Lomkit\Rest\Confirmation\Confirmation;
class ShredDocuments extends Action
{
public function confirmation(ActionFields $fields, int $rows): ?Confirmation
{
if ($rows < 3) {
return null;
}
return Confirmation::make("Shred {$rows} documents?")
->data(['rows' => $rows])
->expiresIn(300);
}
}
make()takes the message of the response.data()adds an object for the client. It takes an array with string keys,{}when empty. A list throwsLomkit\Rest\Exceptions\InvalidConfirmationException.expiresIn()sets the lifetime of the token in seconds,300by default, at least1.$rowscounts the rows that the search matches in the perimeter, within itslimit, the keys of theResourcestarget, or0for theNonetarget. It is counted beforeskipUnauthorized()leaves rows out, soimpactedcan end lower.
The first request
POST /api/documents/actions/shred-documents
{"search": {}}
The endpoint validates the body, runs the entry checks and the row checks, then answers 428 and runs nothing:
{
"message": "Shred 4 documents?",
"confirmation": {
"token": "1794132300.f70157796d502916907407d48c8d213b83d718f1c91d9723258bf7760aac3331",
"expires_at": "2026-11-08T10:05:00Z",
"data": {"rows": 4}
}
}
No operating() event, no handle(), no job, no batch and no ActionOperated event run before the confirmation. A 422 and a 403 still come first.
Confirming
The client sends the same body again, with the token under confirmation:
{"search": {}, "confirmation": "1794132300.f70157796d502916907407d48c8d213b83d718f1c91d9723258bf7760aac3331"}
{"data": {"impacted": 4, "result": null}}
The token is valid for the same user and guard, the same resource and action, the same search, resources and fields, and the same number of rows, until it expires. It can be used again until then.
- The order of the keys and the unknown keys at the first level of the body do not count.
- Any other token, an expired one, or a number of rows that changed answers a new
428with a fresh token. With one more row, the message becomesShred 5 documents?. confirmationmust be a string: another type,nullincluded, is a422,The confirmation field must be a string.
app.key. Set APP_KEY before an action or a resource asks for a confirmation. An empty key throws Lomkit\Rest\Exceptions\InvalidConfigException once a confirmation is due. A hook that returns null needs no key.Confirming writes
A resource asks for a confirmation before a write with one hook per endpoint. Each returns a Confirmation, or null, the default, to write at once:
| Hook | Endpoint | Receives |
|---|---|---|
mutateConfirmation(PendingMutation $mutation) | mutate | the entries of the request |
destroyConfirmation(Collection $models) | destroy | the rows of resources |
restoreConfirmation(Collection $models) | restore | the rows of resources |
forceDestroyConfirmation(Collection $models) | forceDestroy | the rows of resources |
use Illuminate\Database\Eloquent\Collection;
use Lomkit\Rest\Confirmation\Confirmation;
public function destroyConfirmation(Collection $models): ?Confirmation
{
return Confirmation::make("Delete {$models->count()} documents?")
->data(['titles' => $models->pluck('title')->all()]);
}
{"resources": [1, 2]}
{
"message": "Delete 2 documents?",
"confirmation": {
"token": "1794132300.…",
"expires_at": "2026-11-08T10:05:00Z",
"data": {"titles": ["Budget", "Contract"]}
}
}
The client sends the same body again with "confirmation": "<token>", and the endpoint writes.
Lomkit\Rest\Mutations\PendingMutation holds the entries of mutate, as MutationNode objects, and the stored rows that they name:
use Illuminate\Database\Eloquent\Model;
use Lomkit\Rest\Confirmation\Confirmation;
use Lomkit\Rest\Fields\MutationOperation;
use Lomkit\Rest\Mutations\MutationNode;
use Lomkit\Rest\Mutations\PendingMutation;
public function mutateConfirmation(PendingMutation $mutation): ?Confirmation
{
$archived = collect($mutation->entries)
->filter(fn (MutationNode $entry): bool => $entry->operation === MutationOperation::Update)
->map(fn (MutationNode $entry): ?Model => $mutation->row($this, $entry->key))
->filter(fn (?Model $document): bool => (bool) $document?->archived);
return $archived->isEmpty() ? null : Confirmation::make("Edit {$archived->count()} archived documents?");
}
$mutation->entrieslists the entries of the root ofmutate. Theirrelationshold the nested entries.$mutation->row($resource, $key)returns the row of a key as stored before the request, ornull.
When the checks run:
mutateasks after validation (422), the row checks (403) and the version check (409), inside its transaction, beforemutatingAll().destroy,restoreandforceDestroyask after validation and the row checks, before their transaction.- A
428writes nothing: nomutatingAll(),destroyingAll(),restoringAll()orforceDestroyingAll(), no row hook and no event.
The token is valid for the same user and guard, the same resource and endpoint, and the same body: mutate and response on mutate, resources on the other endpoints. A token of forceDestroy does not confirm a destroy. The order of the keys of an object and the unknown keys at the first level of the body do not count, but the order of a list does: {"resources": [2, 1]} asks again.
The four endpoints read confirmation as a string, also on a resource without the hook: another type is a 422.
Clients
- The details, with
?with=actions, give"confirmation": trueon each action that declaresconfirmation(). With?with=confirmations, they give the writes of the resource that ask for one:{"mutate": false, "destroy": true, "restore": false, "forceDestroy": false}. Show your dialog when the endpoint answers428, with themessageand thedataof the response. - A Precognition request stops before the confirmation: it answers
204or422and asks nothing. - The OpenAPI document lists the
428response,RestConfirmationRequired, on the operations of these actions and writes, and documentsconfirmationin their body. See OpenAPI.