My trash — the soft-deleted items I own, or the ones I deleted
GET /account/trash
One mixed, newest-deleted-first list of everything the caller
OWNS that is currently soft-deleted, across all three
soft-deletable entities. Each row carries the kind needed to
pick the matching restore endpoint.
scope=deleted_by_me answers the OTHER half of the same
question (#981). CanRestoreDeleted grants the undo to whoever
performed the delete, so a team lead who removes a colleague’s
asset may restore it — but the default scope is owner-scoped,
so that item lists in the OWNER’s trash (non-restorable for
them, since they did not delete it) and in nobody’s as
restorable. The lead held a restore right they could not reach.
This scope lists exactly the rows where
deleted_by_user_ref = caller AND owner IS DISTINCT FROM caller
— the caller’s own deletions of other people’s things — and
every one of them comes back restorable_by_caller: true by
the same predicate, because the deleter is the caller.
IT IS NOT A PROBE. The two scopes disclose on different
grounds, and this one’s ground is the caller’s own prior act:
the delete already succeeded for this caller against this row,
which means they could both see it and mutate it at that
moment. The response therefore reveals nothing they did not
already have. It also cannot be steered — there is no
user/owner filter, only the caller’s own ref taken from
the session — so it lists a caller’s history, never a search
over someone else’s content. The projection is byte-for-byte
the one the owned scope returns; in particular it does NOT add
the owner’s identity, which would be a disclosure the delete
itself never made.
WHY THIS EXISTS SEPARATELY FROM include_deleted=true (#937).
GET /assets, GET /posts and GET /collections all accept
include_deleted=true and all silently drop it for anyone
below system.admin. That is deliberate and stays: those
listings are not owner-scoped, so honouring the flag for an
ordinary caller would turn each of them into a probe for
deleted rows that caller has no other way to observe. Self-
service restore (#936) shipped nonetheless, which left the
owner able to restore items but with no way to learn their
ids. This endpoint closes that without touching the admin
flag: it answers a strictly narrower question — “what of MINE
is in the bin” — and so cannot disclose anything the caller
did not already know existed.
restorable_by_caller is the same decision the matching
POST /admin/{assets,posts,collections}/{id}/restore will
make, computed by the same predicate rather than by a second
copy of the rule, so the button this list renders can never
disagree with the endpoint it calls. It is FALSE when someone
else deleted the item — an administrator moderating it, or a
team-mate with mutation rights — and also false when the
deleter was never recorded (rows predating migration 00037,
and system-scheduled retention deletes), because both fail
closed to system.admin. The identity of the deleter is NOT
returned: the owner needs to know whether they may undo it,
not who did it, and deleted_by_user_ref stays server-side.
Ordering and pagination are the (deleted_at, id) keyset,
DESC, with the id tiebreak carrying the same comparison as the
timestamp — items deleted in one batch share a deleted_at
almost exactly, so the tiebreak is the common case here, not
the rare one.
Authorizations
Section titled “Authorizations ”Parameters
Section titled “ Parameters ”Query Parameters
Section titled “Query Parameters ”owned_by_me (default) — soft-deleted items the caller
owns, whoever deleted them. deleted_by_me — items the
caller deleted that they do NOT own; always restorable by
them. The two sets are disjoint by construction, so a
client showing both as tabs never double-lists a row.
Opaque token from a previous response’s next_cursor.
Responses
Section titled “ Responses ”A page of the caller’s own soft-deleted items.
object
One soft-deleted thing the caller owns (#937). Deliberately a thin projection rather than the entity itself: trash needs to identify an item and offer to undo, and shipping the full Asset/Post/Collection here would make three read contracts reachable through a fourth door with a different gate.
object
Which entity this is, and therefore which restore endpoint
applies: /admin/assets/{id}/restore,
/admin/posts/{id}/restore or
/admin/collections/{id}/restore. (Those routes live under
/admin for historical reasons; since #936 they admit the
deleter as well as system.admin.)
The item’s title — collections.name for a collection.
May be empty: assets and posts both default title to ”.
True when this caller may restore this item right now. False means an administrator or another user with mutation rights deleted it, or no deleter was recorded — either way the owner must ask (#931). Renders the difference between a Restore button and an explanation; never a button that 403s.
The reason the deleter typed, if any (#985). Null when none was given.
Disclosed in BOTH scopes, for different reasons. In
owned_by_me it is the promise the delete dialog already
makes to whoever writes it — “you don’t own this, so the
owner will be shown what you write here” — and until #931
the field was not projected, so that promise was never
kept. In deleted_by_me it is simply the caller’s own
words read back.
True when this caller already has a pending restoration
appeal filed against this row (#931), so the UI shows a
quiet “requested” state instead of an Appeal button that
would coalesce onto the existing row and look like nothing
happened.
Only ever about the CALLER’s own request. It is not a count and does not disclose that anyone else has appealed.
When the retention GC becomes eligible to hard-delete this
item: deleted_at plus the configured retention window for
its kind (soft_delete.{asset,post,collection}_retention_days,
default 30). Advisory, not a guarantee — the GC wakes once a
day and works in batches, so the real purge lands at or
after this instant. Absent only if the window could not be
read.
Opaque token for the next page; omitted on the final page.
Pass back unchanged as the cursor query parameter.
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"}Unexpected server error
object
Human-readable error summary
Example
the request could not be completed