Skip to content

Appeal a delete you cannot undo yourself

POST
/account/trash/{kind}/{id}/request-restore

Files a pending resource_request asking whoever deleted this item to put it back (#931).

This is the OTHER half of auth.CanRestoreDeleted. That rule gives the undo to the deleter, and to system.admin — so an owner whose work an administrator removed sees a trash row that says someone else deleted it and, before this endpoint, was offered nothing further. The row was a dead end.

The capability is not an input

The server writes content.restore.request and there is no field on this body to say otherwise. requested_capability feeds an authorisation decision, and ADR 0064 already names the hazard of taking it from the requester: “nothing stops a request naming system.admin”. /assets/{id}/request-access still ACCEPTS an explicit code for admin tooling that predates the button; this endpoint accepts none at all, because there is no second thing an appeal could ask for.

Who may file one

The item’s owner, and only while the item is soft-deleted. Someone who could restore it directly gets 409 rather than a request nobody needs to answer — the honest reply to “please ask them to undo it” when you may undo it yourself is “you can do that”. Any other caller gets 404: an appeal endpoint that distinguished “not yours” from “no such row” would be a probe for other people’s deleted items.

Repeating an appeal you already have pending returns 200 with the existing row, unchanged, matching /assets/{id}/request-access. One ask is (requester, kind, id, capability); asking twice is the same question, and a second row would make the decider deny one of them.

What granting it does

Performs the restore. The grant branch calls the same softdelete primitive the per-kind restore endpoints call and writes NO user_capability_grants row — the marker confers nothing and there is nothing to confer, because the outcome the requester asked for is a state change on the item, not a standing right. See POST /admin/requests/{id}/decide.

kind
required
string
Allowed values: asset post collection

Which soft-deletable entity id names.

id
required
string format: uuid
object
reason

Optional free text for the decider — why this should come back.

string
<= 1000 characters

An identical appeal 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

Appeal filed.

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"
}

No such soft-deleted row of that kind, or it is not the caller’s. One response for both, deliberately.

object
error
required

Human-readable error summary

string
Example
the request could not be completed

The caller may already restore this item directly (they deleted it, or they hold system.admin). Nothing to appeal.

object
error
required

Human-readable error summary

string
Example
the request could not be completed