Marketplace, files, and moderation endpoints
The 2026-05-29 expansion added programmatic access to the marketplace listings, file uploads, and the moderation queue. Every endpoint here is scoped, paginated, and tenant-isolated like the rest of the v1 surface.
Marketplace listings
GET /api/v1/listings
Lists every listing on the tenant, newest first.
Query parameters:
| Param | Type | Example |
|---|---|---|
status |
string | active, pending, sold, withdrawn, expired |
seller_id |
integer | 312 |
category_id |
integer | 7 |
cursor |
string | opaque |
limit |
integer | 25 (max 100) |
Response shape:
{
"data": [
{
"id": 42, "slug": "atari-2600-light-sixer",
"title": "Atari 2600 Light Sixer",
"description": "Working, original box…",
"price_cents": 12500,
"quantity": 1, "quantity_remaining": 1,
"condition_rating": "good",
"location": { "country": "US", "region": "NY", "city": "Brooklyn" },
"status": "active",
"category": { "id": 7, "name": "Atari 2600" },
"seller": { "id": 312, "username": "retroseller" },
"created_at": "2026-05-29T13:00:00Z",
"expires_at": "2026-06-28T13:00:00Z"
}
],
"next_cursor": "NDI=",
"request_id": "req_…"
}
GET /api/v1/listings/{id}
Returns one listing by id with the same shape.
Scope required: listings:read.
File uploads
GET /api/v1/files
Lists every approved file on the tenant. Files still in quarantine are not exposed.
Query parameters:
| Param | Type | Example |
|---|---|---|
area_slug |
string | atari-2600-disks |
uploader_id |
integer | 312 |
cursor |
string | opaque |
limit |
integer | 25 (max 100) |
Response shape:
{ "data": [{
"id": 18, "title": "Centipede (1981)",
"description": "Atari Centipede dump.",
"original_name": "centipede.bin",
"mime_type": "application/octet-stream",
"size_bytes": 16384,
"download_count": 4,
"area": { "slug": "atari-2600-disks", "name": "Atari 2600 disks" },
"uploader": { "id": 312, "username": "retroseller" },
"created_at": "2026-05-29T13:05:00Z"
}], "next_cursor": null, "request_id": "req_…" }
GET /api/v1/files/{id}
Returns one approved file by id.
Scope required: files:read.
Moderation
GET /api/v1/reports
Lists moderation reports. Filter by status or reported_user_id.
Description is omitted from the list response for size; fetch a single report to get the full body.
GET /api/v1/reports/{id}
Returns a single report including the full description field.
POST /api/v1/reports/{id}/dismiss
Marks the report status='resolved' with resolution_notes prefixed Dismissed via API:. Body:
{ "reason": "No violation found." }
Returns 400 already_resolved if the report is already resolved.
POST /api/v1/reports/{id}/resolve
Same shape as dismiss but the resolution notes are prefixed Resolved via API:. Use this when the report is valid and you took some action externally that you want recorded.
GET /api/v1/moderation/actions
Reads the mod_log table — every moderator action ever taken on the tenant. Filter by action (e.g. forum.ban, moderation.warn), actor_id, or affected_user_id.
Scopes:
reports:read—GETendpoints + the moderation-actions logreports:manage— the twoPOSTendpoints (dismiss, resolve)
Best practices
- Page in batches: cursor pagination is forward-only; persist the last
next_cursoryou saw so you can resume without re-walking. - Read with the smallest scope: a key with only
reports:readcannot accidentally close a report. - Subscribe to webhooks for
listing.*,file.uploaded,report.created, andmoderation.action_takenrather than polling — webhooks are delivered within ~60 seconds of the underlying write.
# Marketplace, files, and moderation endpoints
The 2026-05-29 expansion added programmatic access to the marketplace listings, file uploads, and the moderation queue. Every endpoint here is scoped, paginated, and tenant-isolated like the rest of the v1 surface.
## Marketplace listings
### `GET /api/v1/listings`
Lists every listing on the tenant, newest first.
Query parameters:
| Param | Type | Example |
|----------------|----------|----------------------|
| `status` | string | `active`, `pending`, `sold`, `withdrawn`, `expired` |
| `seller_id` | integer | `312` |
| `category_id` | integer | `7` |
| `cursor` | string | opaque |
| `limit` | integer | 25 (max 100) |
Response shape:
```json
{
"data": [
{
"id": 42, "slug": "atari-2600-light-sixer",
"title": "Atari 2600 Light Sixer",
"description": "Working, original box…",
"price_cents": 12500,
"quantity": 1, "quantity_remaining": 1,
"condition_rating": "good",
"location": { "country": "US", "region": "NY", "city": "Brooklyn" },
"status": "active",
"category": { "id": 7, "name": "Atari 2600" },
"seller": { "id": 312, "username": "retroseller" },
"created_at": "2026-05-29T13:00:00Z",
"expires_at": "2026-06-28T13:00:00Z"
}
],
"next_cursor": "NDI=",
"request_id": "req_…"
}
```
### `GET /api/v1/listings/{id}`
Returns one listing by id with the same shape.
Scope required: **`listings:read`**.
## File uploads
### `GET /api/v1/files`
Lists every **approved** file on the tenant. Files still in quarantine are not exposed.
Query parameters:
| Param | Type | Example |
|---------------|---------|-----------------------|
| `area_slug` | string | `atari-2600-disks` |
| `uploader_id` | integer | `312` |
| `cursor` | string | opaque |
| `limit` | integer | 25 (max 100) |
Response shape:
```json
{ "data": [{
"id": 18, "title": "Centipede (1981)",
"description": "Atari Centipede dump.",
"original_name": "centipede.bin",
"mime_type": "application/octet-stream",
"size_bytes": 16384,
"download_count": 4,
"area": { "slug": "atari-2600-disks", "name": "Atari 2600 disks" },
"uploader": { "id": 312, "username": "retroseller" },
"created_at": "2026-05-29T13:05:00Z"
}], "next_cursor": null, "request_id": "req_…" }
```
### `GET /api/v1/files/{id}`
Returns one approved file by id.
Scope required: **`files:read`**.
## Moderation
### `GET /api/v1/reports`
Lists moderation reports. Filter by `status` or `reported_user_id`.
Description is omitted from the list response for size; fetch a single report to get the full body.
### `GET /api/v1/reports/{id}`
Returns a single report **including the full `description`** field.
### `POST /api/v1/reports/{id}/dismiss`
Marks the report `status='resolved'` with `resolution_notes` prefixed `Dismissed via API:`. Body:
```json
{ "reason": "No violation found." }
```
Returns 400 `already_resolved` if the report is already resolved.
### `POST /api/v1/reports/{id}/resolve`
Same shape as dismiss but the resolution notes are prefixed `Resolved via API:`. Use this when the report is valid and you took some action externally that you want recorded.
### `GET /api/v1/moderation/actions`
Reads the `mod_log` table — every moderator action ever taken on the tenant. Filter by `action` (e.g. `forum.ban`, `moderation.warn`), `actor_id`, or `affected_user_id`.
Scopes:
- `reports:read` — `GET` endpoints + the moderation-actions log
- `reports:manage` — the two `POST` endpoints (dismiss, resolve)
## Best practices
- **Page in batches**: cursor pagination is forward-only; persist the last `next_cursor` you saw so you can resume without re-walking.
- **Read with the smallest scope**: a key with only `reports:read` cannot accidentally close a report.
- **Subscribe to webhooks** for `listing.*`, `file.uploaded`, `report.created`, and `moderation.action_taken` rather than polling — webhooks are delivered within ~60 seconds of the underlying write.