Skip to content

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.

scope
string
default: owned_by_me
Allowed values: owned_by_me deleted_by_me

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.

limit
integer
default: 50 >= 1 <= 200
cursor
string

Opaque token from a previous response’s next_cursor.

A page of the caller’s own soft-deleted items.

object
items
required
Array<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
kind
required

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.)

string
Allowed values: asset post collection
id
required
string format: uuid
title
required

The item’s title — collections.name for a collection. May be empty: assets and posts both default title to ”.

string
deleted_at
required
string format: date-time
restorable_by_caller
required

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.

boolean
deleted_reason

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.

string
nullable
restore_requested
required

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.

boolean
purge_after

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.

string format: date-time
nullable
next_cursor

Opaque token for the next page; omitted on the final page. Pass back unchanged as the cursor query parameter.

string
nullable

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

Unexpected server error

object
error
required

Human-readable error summary

string
Example
the request could not be completed