Grant or deny a pending resource request
POST /admin/requests/{id}/decide
Atomic CAS — only pending requests transition. Re-deciding an already-decided request returns 409.
There are two kinds of request behind this one endpoint, and
requested_capability — not the caller’s rank — decides which
gate and which consequence apply.
Access requests (anything except content.restore.request)
Requires share.grant OR system.admin — or ownership of
the requested asset, for requests naming
content.access.request only. The owner disjunct is narrower on
purpose: requested_capability is requester-controlled, so an
owner who could decide any request could be induced to grant
system.admin from a panel about their own artwork.
On grant, a user_capability_grants row is inserted in the same tx as the resource_request CAS. expires_at on the grant is the same value the operator supplied here; the CapabilitySweeper reaps both together when it fires.
A grant does not unlock the asset’s bytes. There is no per-object capability scope, so approval records agreement, not access — see #912 and ADR 0064.
Restoration appeals (content.restore.request, #931)
Decidable ONLY by a caller who passes auth.CanRestoreDeleted
against the TARGET’s deleter: the person who deleted it, or
system.admin. share.grant alone does not qualify —
authority over sharing is not authority over moderation — and
neither does ownership, so a requester can never decide their
own appeal.
On grant the server performs the restore and writes no
user_capability_grants row. The marker confers nothing and
there is nothing to confer: what was asked for is a state change
on the item, not a standing right. This is also what keeps the
#881 escalation surface closed — the grant path that copies
requested_capability verbatim into a capability grant is not
reached on this branch at all.
expires_at on a restore grant is 400. A performed restore
cannot expire, and silently ignoring the field would let an
operator believe they had set a deadline.
If someone else already restored the target, the grant still succeeds: the requested end-state holds, and the decision reason records that it was already live.
Failure shape
A caller who is neither an approver nor the owner gets 403 for every failure, including a request id that does not exist: a 404 here would let a non-approver probe for request ids and read off which capability each names.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Request Body required
Section titled “Request Body required ”object
Operator-supplied decision reason.
On grant: optional auto-expiry. Past values rejected. Omit / null for a permanent grant. Ignored on deny.
Responses
Section titled “ Responses ”Decided.
A resource_request row. The state machine is pending → granted / denied (admin decision); granted → expired (CapabilitySweeper cascade). Terminal states admit no further transitions.
object
Which table target_id refers to. Access requests (#881)
are always asset; restoration appeals (#931) may name any
of the three soft-deletable kinds.
The requested object, in the table named by target_kind.
Called target_asset_id before #931, when the workflow
could only ever name an asset.
The capability code this request names. Two of them are inert markers that confer nothing and exist only to TYPE the row for its decide gate:
content.access.request— “may I see this asset” (#881). Decidable by the asset’s owner,share.grant, orsystem.admin.content.restore.request— “please undo this delete” (#931). Decidable ONLY by whoever deleted the target, or bysystem.admin.share.grantdoes not qualify: authority over sharing is not authority over moderation. Granting one performs the restore; it writes no capability grant.
True when THIS caller may decide THIS row right now,
computed by the same gate the decide endpoint applies.
Populated on the queue listings (/admin/requests,
/account/requests/incoming) and absent elsewhere.
It exists because the two queues have different gates and
the admin queue is readable by people who cannot act on
every row it contains: share.grant sees restoration
appeals but may not decide them. Rendering a decide control
from row presence alone would offer a button that 403s.
On granted requests, the linked grant’s auto-expiry. Null means permanent.
Malformed request
object
Human-readable error summary
Example
the request could not be completedAuthentication required, missing, or invalid
object
Human-readable error summary
Example
the request could not be completedExample
{ "error": "authentication required: sign in and retry with a valid session or API token"}Authenticated but missing required capabilities
object
Human-readable error summary
Example
the request could not be completedResource not found
object
Human-readable error summary
Example
the request could not be completedAlready decided.
object
Human-readable error summary
Example
the request could not be completed