--- title: "DevPlace REST: Reporting and Moderation Queue" description: "The polymorphic report endpoint, the machine-readable reason list, and the administrator moderation queue: its state machine, actions, seniority rule and acknowledgement contract with the published review window." language: null framework: null category: api_design tags: - devplace - api - http-api - moderation - reporting - admin - rest - state-machine - trust-and-safety keywords: - devplace GET /reports/reasons - devplace POST /reports polymorphic target - devplace admin moderation queue endpoints - devplace moderation state machine statuses - devplace admin seniority rule moderation last_updated: 2026-08-12 difficulty: intermediate version: "DevPlace (devplace.net), documented 2026-08" related: - conventions_and_errors.md - authentication.md - README.md search_priority: normal status: published --- # DevPlace REST: Reporting and Moderation Queue Every externally visible surface on DevPlace is reportable through one polymorphic endpoint, and every report lands in one queue with one state machine. The reason list is served by `GET /reports/reasons`, so a native client renders the same dialog the web UI does. DevPlace commits to reviewing every report within the window published on the [content moderation](https://devplace.net/docs/#doc-content-moderation) page. Filing a report always returns an acknowledgement carrying that window. The moderation endpoints under `/admin/moderation` are administrator-only and are subject to the admin seniority rule: a junior administrator cannot action a more senior one. Every endpoint follows the shared [Conventions & Errors](conventions_and_errors.md) (auth, content negotiation, pagination, status codes); see [Authentication](authentication.md) for the four ways to sign requests. ## `GET /reports/reasons` - List report reasons The reason keys a report may be filed under, with their labels. *Minimal role:* Public **Sample response** ```json { "reasons": [ { "key": "harassment", "label": "Harassment or bullying" } ], "severities": [ "info", "warn", "critical" ] } ``` ## `POST /reports/{target_type}/{target_uid}` - Report content File a report against any user-generated surface. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `target_type` | path | enum | yes | The kind of content being reported. Allowed: post, comment, gist, project, project_file, news, attachment, message, quiz, poll, award, user, issue, workspace, devii_output. | | `target_uid` | path | string | yes | UID of the reported item. | | `reason` | form | enum | yes | Why the content breaks the guidelines. Allowed: hate, violence, weapons, sexual, religious, misinformation, exploitative, harassment, spam, intellectual_property, self_harm, illegal, other. | | `detail` | form | string | no | Free text for the moderator, up to 2000 characters. | > A second report on the same target by the same reporter updates the open report instead of creating a duplicate. > You cannot report your own content. **Sample response** ```json { "ok": true, "redirect": "/reports/mine", "data": { "uid": "REPORT_UID", "status": "open", "severity": "warn", "sla_hours": 24 } } ``` ## `GET /reports/mine` - List your reports The reports you filed and the outcome of each. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `status` | query | enum | no | Filter by report status. Allowed: open, acknowledged, actioned, dismissed. | | `page` | query | integer | no | Page number. | **Sample response** ```json { "reports": [ { "uid": "REPORT_UID", "target_type": "post", "target_uid": "POST_UID", "target_url": "/posts/a-post", "reason": "harassment", "reason_label": "Harassment or bullying", "detail": "Repeated personal attacks in the thread.", "severity": "warn", "status": "open", "origin": "member", "categories": [], "created_at": "2026-01-05T10:00:00+00:00", "resolved_at": "", "reporter_name": "alice", "owner_name": "bob", "report_count": 2 } ], "pagination": { "page": 1, "total": 1, "total_pages": 1 }, "status": "" } ``` ## `GET /admin/moderation` - The moderation queue Reported content awaiting a decision, oldest open first. *Minimal role:* Admin **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `status` | query | enum | no | Filter by report status. Allowed: open, acknowledged, actioned, dismissed. | | `page` | query | integer | no | Page number. | **Sample response** ```json { "reports": [ { "uid": "REPORT_UID", "target_type": "post", "target_uid": "POST_UID", "target_url": "/posts/a-post", "reason": "harassment", "reason_label": "Harassment or bullying", "detail": "Repeated personal attacks in the thread.", "severity": "warn", "status": "open", "origin": "member", "categories": [], "created_at": "2026-01-05T10:00:00+00:00", "resolved_at": "", "reporter_name": "alice", "owner_name": "bob", "report_count": 2 } ], "counts": { "open": 1, "acknowledged": 0, "actioned": 0, "dismissed": 0 }, "sla": { "sla_hours": 24, "oldest_open_hours": 1.5, "breached": 0, "within_sla": true } } ``` ## `GET /admin/moderation/{uid}` - Read one report One report with its decisions and the author's history. *Minimal role:* Admin **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `uid` | path | string | yes | Report UID. | **Sample response** ```json { "report": { "uid": "REPORT_UID", "target_type": "post", "target_uid": "POST_UID", "target_url": "/posts/a-post", "reason": "harassment", "reason_label": "Harassment or bullying", "detail": "Repeated personal attacks in the thread.", "severity": "warn", "status": "open", "origin": "member", "categories": [], "created_at": "2026-01-05T10:00:00+00:00", "resolved_at": "", "reporter_name": "alice", "owner_name": "bob", "report_count": 2 }, "actions": [], "history": [], "available_actions": [ "remove_content", "restore_content", "warn", "suspend", "ban", "lift", "dismiss", "escalate" ], "can_remove": true } ``` ## `POST /admin/moderation/{uid}/status` - Set a report status Move a report through the triage state machine. *Minimal role:* Admin **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `uid` | path | string | yes | Report UID. | | `status` | form | enum | yes | New status. Allowed: open, acknowledged, actioned, dismissed. | **Sample response** ```json { "ok": true, "redirect": "/admin/moderation/REPORT_UID" } ``` ## `POST /admin/moderation/{uid}/decide` - Decide a report Apply a moderation decision and notify the affected user. *Minimal role:* Admin **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `uid` | path | string | yes | Report UID. | | `action` | form | enum | yes | The decision to apply. Allowed: remove_content, restore_content, warn, suspend, ban, lift, dismiss, escalate. | | `reason` | form | string | no | Reason shown to the affected user. | | `notes` | form | string | no | Internal notes. | | `duration_hours` | form | integer | no | Suspension length in hours. | > A report already resolved by another moderator answers 409. > Content removal is unavailable for targets that have no removal path (direct messages, accounts, workspaces, polls, assistant output); act on the account instead. **Sample response** ```json { "ok": true, "redirect": "/admin/moderation/REPORT_UID" } ``` ## `POST /admin/users/{uid}/suspend` - Suspend an account Suspend an account for a fixed period with a stated reason. *Minimal role:* Admin **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `uid` | path | string | yes | User UID. | | `reason` | form | string | no | Reason shown to the user. | | `duration_hours` | form | integer | no | Suspension length in hours. | > A suspended account can still read, still see why, and still delete itself, but cannot create content. **Sample response** ```json { "ok": true, "redirect": "/admin/users" } ``` ## `POST /admin/users/{uid}/lift` - Lift a restriction Clear a suspension or ban and restore the account. *Minimal role:* Admin **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `uid` | path | string | yes | User UID. | **Sample response** ```json { "ok": true, "redirect": "/admin/users" } ``` ## `POST /admin/users/{uid}/ban` - Ban an account Permanently close an account and revoke every credential. *Minimal role:* Admin **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `uid` | path | string | yes | User UID. | | `reason` | form | string | no | Reason shown to the user. | **Sample response** ```json { "ok": true, "redirect": "/admin/users" } ``` ## `GET /auth/accept-terms` - The terms acceptance page The Terms of Service version in force and the version this account accepted. *Minimal role:* Member **Sample response** ```json { "terms_version": "1", "accepted_version": "" } ``` ## `POST /auth/accept-terms` - Accept the terms Record acceptance of the Terms of Service version in force. *Minimal role:* Member > A member whose accepted version is behind the version in force is redirected here on any mutating request. Reading, the docs, the safety controls and account deletion are never blocked. **Sample response** ```json { "ok": true, "redirect": "/feed", "data": { "terms_version": "1" } } ``` ## `POST /profile/{username}/consent` - Grant or withdraw a consent Change one consent on your own account. Withdrawal takes effect at once. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `username` | path | string | yes | Your own username. | | `kind` | form | enum | yes | Consent to change. Allowed: terms, privacy, ai_third_party, activity_recording, container_credentials. | | `granted` | form | enum | yes | 1 grants, 0 withdraws. Allowed: 1, 0. | > Withdrawing `ai_third_party` makes the AI gateway refuse every call that would send your own content to the provider, whatever the per-feature preference says. > Withdrawing `activity_recording` stops presence writes; you simply appear offline. > Only the account holder can change a consent. An administrator reads the record but never grants or withdraws it for someone else. **Sample response** ```json { "ok": true, "redirect": "/profile/USERNAME?tab=privacy", "data": { "kind": "ai_third_party", "state": "granted" } } ``` ## `POST /profile/{username}/mature-content` - Set the mature-content preference Show or hide content labelled mature for your own account. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `username` | path | string | yes | Your own username. | | `mature_opt_in` | form | enum | yes | 1 shows mature content, 0 hides it. Allowed: 1, 0. | > Only the account holder can change this preference. An administrator reads the privacy tab but never sets it for someone else. **Sample response** ```json { "ok": true, "redirect": "/profile/USERNAME?tab=privacy", "data": { "mature_opt_in": true } } ``` ## `GET /profile/{username}/delete` - Account deletion page What deletion removes, what is retained, and the grace window. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `username` | path | string | yes | Your own username. | **Sample response** ```json { "username": "USERNAME", "grace_hours": 24, "removed": [ "Your account record, username, email address and password" ], "retained": [ "Append-only audit and moderation records" ] } ``` ## `POST /profile/{username}/delete` - Delete your account Permanently delete your account and personal data. *Minimal role:* Member **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `username` | path | string | yes | Your own username. | | `password` | form | string | yes | Your account password. | > Only the account holder can delete an account; an administrator uses a ban instead. > Sessions and tokens are revoked and the profile is anonymised immediately; the deletion event is purged after the grace window. **Sample response** ```json { "ok": true, "redirect": "/", "data": { "stamp": "2026-01-05T10:00:00+00:00", "rows": 42, "grace_hours": 24 } } ``` ## `GET /workspaces/index` - Published workspace index Every workspace published to the public ingress, with its link. *Minimal role:* Public **Parameters** | Name | In | Type | Required | Description | |------|----|------|----------|-------------| | `page` | query | integer | no | Page number. | > The project-derived `description` and `project_url` come back empty unless you may view the workspace's project, so a private project never leaks its title or description through this public listing. **Sample response** ```json { "workspaces": [ { "uid": "INSTANCE_UID", "name": "demo", "slug": "demo", "owner_uid": "USER_UID", "url": "https://devplace.net/p/demo", "description": "A demo workspace.", "owner": "alice", "maturity": "general", "project_url": "/projects/demo" } ], "total": 1 } ```