Content reports API
The content reports API manages user-filed moderation reports against AI-generated content. Reports are produced by the Report button in the Chat view and reviewed in the Reports view (Overview › Feedback › Reports).
All endpoints require an authenticated admin session.
Listing reports
GET /admin/v1/reports
Required permission: TENANT_SETTINGS_MANAGE — held by default by an admin or tenant admin, and delegable by a tenant admin to a custom role.
The result is tenant-scoped on the server: every caller sees only reports from their own tenant, except a platform admin (the admin role), who sees reports across all tenants. Delegating TENANT_SETTINGS_MANAGE to a custom role never widens this — a custom-role holder is still confined to their own tenant. Paging (offset/limit) does not bypass the scope: a high offset returns later pages of the caller's own tenant only, never another tenant's rows.
Optional query parameters:
| Parameter | Description |
|---|---|
status |
Filter by status: open, triaged, or dismissed. |
limit |
Maximum reports to return. Default 100, clamped to 500. |
offset |
Number of reports to skip, for paging. Default 0. |
The response is an array of report objects with the fields listed below.
Updating a report
PATCH /admin/v1/reports/<ID>
Required permission: TENANT_SETTINGS_MANAGE.
The same tenant scope applies: a non-platform caller may update only a report that belongs to their own tenant. A PATCH targeting a report in another tenant is rejected with 403 and the row is left unchanged. A platform admin may update a report in any tenant.
Validation. status must be exactly open, triaged, or dismissed; an absent, null, or unknown value is rejected 400 {"error":"invalid status"} and nothing is written. An unknown <ID> returns 404 (report not found); a transient database fault returns 503 (retryable).
| Field | Type | Description |
|---|---|---|
status |
open | triaged | dismissed |
The new status. |
When the status changes from open to triaged or dismissed, the server sets the triaged_at timestamp and the triaged_by_id field to the calling user's ID.
Filing a report
POST /admin/v1/reports
Required role: authenticated.
| Field | Type | Required | Description |
|---|---|---|---|
reason |
offensive | inaccurate | unsafe | other |
Yes | The reason flag. Rejected with 400 if missing, null, or not a recognised reason. |
conversation_id |
string | No | Source conversation. Stored in a 36-char id column; a null, blank, or over-36-byte value is dropped to null (the report still files) rather than persisted or truncated to a wrong id. |
message_id |
string | No | Reported message identifier. Same 36-byte cap and drop-to-null handling as conversation_id. |
message_text |
string | No | Snapshot of the reported message body. Truncated to 16 000 bytes on a UTF-8 codepoint boundary (never mid-character); invalid UTF-8 bytes are scrubbed to U+FFFD; null/blank → SQL NULL. |
notes |
string | No | Free-form notes from the reporter. Truncated to 2 000 bytes on a codepoint boundary; invalid UTF-8 scrubbed; null/blank → SQL NULL. |
request_log_id |
string | No | Identifier of the related request-log row. Must match [A-Za-z0-9-]; truncated to 36 characters. |
client_context |
object | No | Client-side context (page, build, etc.) captured by the SPA. Rejected with 400 if malformed; string values are UTF-8-scrubbed before storage. |
Response: 201 { "id": "..." }
Every free-text and identifier field is sanitized at this trust boundary: a JSON null (which is truthy server-side) never persists a literal "userdata: NULL", an over-long or invalid value never reaches the database as-is, and no input path returns a 5xx.
💡 Note: Each filed report is also mirrored, best-effort, into the unified triage queue (
source="content_report"). Mirroring never fails the submission and content reports are not included in the triage notification digest.
Report object fields
| Field | Type | Description |
|---|---|---|
id |
string | Report identifier. |
user_id |
string | The reporting user. |
user_email |
string | null | Email of the reporting user. |
tenant_id |
string | null | Tenant of the source conversation. |
conversation_id |
string | null | Source conversation. |
message_id |
string | null | Reported message identifier. |
message_text |
string | null | A snapshot of the reported message body. |
reason |
offensive | inaccurate | unsafe | other |
The reason flag chosen by the reporter. |
notes |
string | null | Optional free-form notes from the reporter. |
status |
open | triaged | dismissed |
Current status. |
created_at |
unix seconds | Submission timestamp. |
triaged_at |
unix seconds | null | Set when the status leaves open. |
triaged_by_id |
string | null | User who triaged or dismissed the report. |
request_log_id |
string | null | The request-log entry the reported message belongs to, when known. |
request_log_provider |
string | null | Provider that served the reported message. |
request_log_model |
string | null | Model that served the reported message. |
request_log_status |
integer | null | HTTP status of the underlying request. |
request_log_latency_ms |
integer | null | Upstream latency of the underlying request, in milliseconds. |
request_log_input_tokens |
integer | null | Input token count of the underlying request. |
request_log_output_tokens |
integer | null | Output token count of the underlying request. |
client_context |
object | null | Client-supplied context captured with the report. |