Skip to content

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.

id
required
string format: uuid
object
capability

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.

string
reason

Optional free-text justification.

string
<= 1000 characters

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
id
required
string format: uuid
requester_user_ref
required
integer format: int64
target_kind
required

Which table target_id refers to. Access requests (#881) are always asset; restoration appeals (#931) may name any of the three soft-deletable kinds.

string
Allowed values: asset post collection
target_id
required

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.

string format: uuid
requested_capability
required

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, or system.admin.
  • content.restore.request — “please undo this delete” (#931). Decidable ONLY by whoever deleted the target, or by system.admin. share.grant does not qualify: authority over sharing is not authority over moderation. Granting one performs the restore; it writes no capability grant.
string
decidable_by_caller

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.

boolean
reason
string
state
required
string
Allowed values: pending granted denied expired
decided_at
string format: date-time
nullable
decided_by_user_ref
integer format: int64
nullable
decision_reason
string
expires_at

On granted requests, the linked grant’s auto-expiry. Null means permanent.

string format: date-time
nullable
requested_at
required
string format: date-time

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
id
required
string format: uuid
requester_user_ref
required
integer format: int64
target_kind
required

Which table target_id refers to. Access requests (#881) are always asset; restoration appeals (#931) may name any of the three soft-deletable kinds.

string
Allowed values: asset post collection
target_id
required

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.

string format: uuid
requested_capability
required

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, or system.admin.
  • content.restore.request — “please undo this delete” (#931). Decidable ONLY by whoever deleted the target, or by system.admin. share.grant does not qualify: authority over sharing is not authority over moderation. Granting one performs the restore; it writes no capability grant.
string
decidable_by_caller

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.

boolean
reason
string
state
required
string
Allowed values: pending granted denied expired
decided_at
string format: date-time
nullable
decided_by_user_ref
integer format: int64
nullable
decision_reason
string
expires_at

On granted requests, the linked grant’s auto-expiry. Null means permanent.

string format: date-time
nullable
requested_at
required
string format: date-time

Malformed request

object
error
required

Human-readable error summary

string
Example
the request could not be completed

Authentication required, missing, or invalid

object
error
required

Human-readable error summary

string
Example
the request could not be completed
Example
{
"error": "authentication required: sign in and retry with a valid session or API token"
}