Submit a resource request for an asset
POST /assets/{id}/request-access
Creates a pending resource_request for the calling user
against the target asset. Any authenticated user may submit
a request; the asset’s owner — or an approver — decides.
Owners see their queue at GET /account/requests/incoming.
Repeating an ask you already have pending returns 200 with
the existing request, unchanged. One ask is
(requester, asset, capability); asking twice is the same
question, not a second one. A DECIDED request does not block a
new ask — denied is terminal for the row, not for the person.
Granting a request does not currently unlock the asset.
user_capability_grants has no per-object scope, so there is no
capability meaning “you may view this one asset”; a grant here
records that the owner agreed. Per-asset unlocking is tracked
separately (#912) and ADR 0064 documents the deferral. Clients
must not tell the user that approval will reveal the asset.
When the approver grants the request, a row lands in
user_capability_grants with request_ref populated — the
CapabilitySweeper reaps both the grant and the linked
request together at expires_at.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Path Parameters
Section titled “Path Parameters ”Request Body required
Section titled “Request Body required ”object
Capability code being requested. Optional, and
the request-access UI never sends one: it defaults
to content.access.request, an inert marker code
that confers no access on its own.
The field is requester-controlled input, so a
request naming any other code — including
system.admin — is decidable only by a holder of
share.grant or system.admin, never by the
asset’s owner.
content.restore.request is refused here with
400. That marker’s gate is about a soft-deleted
row’s owner and its deleter, and none of those
checks run on this path — see
POST /account/trash/{kind}/{id}/request-restore.
Optional free-text justification.
Responses
Section titled “ Responses ”An identical ask was already pending. The existing request is returned unchanged; nothing was created and nobody was notified again.
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.
Request created.
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"}