---
title: "Resolving requests — Requests for Laravel | Roundly"
description: "Approve, reject and reopen requests with a per-decision reason; every decision lands in the engine’s audit trail."
url: https://roundly-consulting.com/open-source/docs/requests-for-laravel/resolving-requests
language: en
---

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

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

# Resolving requests

Resolve through the facade. Each call takes the request, the acting approver and an optional reason:

```php
use RoundlyConsulting\Requests\Facades\Requests;

$request = Requests::make()->requireApprovalsFrom([$alice, $bob])->create();

Requests::approve($request, $alice);                        // 1 of 2 — stays New
Requests::reject($request, $bob, reason: 'Out of policy');  // unanimous: one rejection rejects it
Requests::reopen($request, $alice);                         // back to New — a fresh approval round opens
Requests::approve($request, $alice);
Requests::approve($request, $bob, reason: 'Looks good');    // 2 of 2 — becomes Approved
```

Every decision is recorded through the approvals engine with its actor, reason and timestamp, so you get a full audit trail for free.

- A request with no declared approvers resolves immediately on the first approve or reject. Repeating its current outcome records the decision and moves nothing, so no second RequestStatusChanged fires.
- Otherwise the approval rule decides when the request flips — by default unanimous, so every declared approver must approve.
- Only the declared approvers — or someone they delegated to — may decide. Anyone else gets RoundlyConsulting\\Approvals\\Exceptions\\UnauthorizedApprovalException and nothing is recorded — straight through the engine too ($mallory->approve($request)).
- Once the approval round has resolved, a new decision would count towards nothing, so approve() and reject() throw RequestAlreadyResolved until the request is reopened (straight through the engine, $alice->approve($request) throws its ClosedApprovalRequestException).
- A request past its expires\_at can no longer be decided, even before requests:expire runs: approve() or reject() expires it on the spot (RequestExpired fires once) and throws RequestAlreadyResolved with status Expired — with or without approvers.
- approve() fires ApprovalRecorded, even when the request stays New.
- reopen() moves the request back to New. While its round is still open (or it has none) the actor’s own decision is withdrawn. If the round is already over, a fresh round opens with the same approvers, rule, stages or preset, and everyone decides again — earlier decisions don’t carry over. A declared approver that no longer exists makes the reopen throw InvalidApprover, and nothing changes (see Reopening below).
- The returned Request is refreshed, so its status already reflects the engine’s outcome.

## Refused decisions

```php
use RoundlyConsulting\Approvals\Exceptions\UnauthorizedApprovalException;
use RoundlyConsulting\Requests\Exceptions\RequestAlreadyResolved;

try {
    Requests::approve($request, $mallory);   // not a declared approver, nor a delegate of one
} catch (UnauthorizedApprovalException $e) {
    // nothing was recorded
}

try {
    Requests::approve($approvedRequest, $alice);   // the round has resolved
} catch (RequestAlreadyResolved $e) {
    $e->status;   // Status::Approved — reopen() first to decide again
}

try {
    Requests::approve($overdueRequest, $alice);    // past expires_at, before requests:expire ran
} catch (RequestAlreadyResolved $e) {
    $e->status;   // Status::Expired — expired on the spot, RequestExpired fired once
}
```

## Reopening

Reopening is a lifecycle move, like cancel() and expire(), not a decision: the actor doesn’t have to be one of the round’s approvers, so your app decides who may reopen. When approvals.authorization.enabled is on, the approvals gate (approvals.authorization.ability, decide-approval by default) is asked as well, whatever state the round is in — a denied actor gets UnauthorizedApprovalException and nothing changes:

```php
use Illuminate\Support\Facades\Gate;
use RoundlyConsulting\Approvals\Exceptions\UnauthorizedApprovalException;

// config/approvals.php — 'authorization' => ['enabled' => true, 'ability' => 'decide-approval']
Gate::define('decide-approval', fn ($user, $approvable) => $user->is_manager);

try {
    Requests::reopen($request, $user);   // the gate is asked whatever state the round is in
} catch (UnauthorizedApprovalException $e) {
    // denied — nothing changed
}

// config/requests.php — 'default_ttl' => 60 * 24 * 7
$expired->status;                        // Status::Expired — its deadline passed
Requests::reopen($expired, $alice);
$expired->expires_at;                    // 7 days from now — null without default_ttl
```

- A request moved back to New whose expires\_at already passed gets a fresh deadline — default\_ttl from now, or none when it isn’t set — so the next sweep doesn’t expire it again at once. A deadline still in the future is kept.
- The reopen and its fresh round happen together on the request’s locked row, so a round that can’t reopen leaves the request as it was.
- ApprovalRevoked is the reopen signal: it fires when the actor’s decision was withdrawn or the request was reopened. RequestStatusChanged fires only when the status actually moved. Reopening a New request the actor never decided fires nothing.

## The ResolveRequest action

The underlying action accepts a Status and an optional reason directly:

```php
use RoundlyConsulting\Requests\Actions\ResolveRequest;
use RoundlyConsulting\Requests\Enums\Status;

app(ResolveRequest::class)->execute($request, $alice, Status::Approved, reason: 'Signed off');
```

## Decisions made through the engine

Because the sync listener mirrors engine resolutions, a decision recorded straight through the approvals engine moves the request too — as long as the actor is a declared approver or their delegate:

```php
use RoundlyConsulting\Approvals\Facades\Approvals;

// Decide straight through the approvals engine — the request's Status still follows.
Approvals::for($request)->as($alice)->approve();

$request->fresh()->status; // Status::Approved once the rule is met
```

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