The /api/admin/* endpoints are restricted to platform administrators. The caller must (a) have an Administrator record AND (b) be the owner of the system organization named The Public. Anything else returns 403 Forbidden. These endpoints are not part of the public API contract and may change without notice.
A related diagnostic surface (the architecture-aggregates endpoints) has the same Public-owner gate but is documented under Utility Endpoints.
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/users | Paginated list of all users. Search across email/username/displayName. |
| POST | /api/admin/users | Create a user (optionally pre-verified, optionally promoted to admin). |
| PUT | /api/admin/users/:userId | Update any subset of user fields (including accountStatus). |
| DELETE | /api/admin/users/:userId | Delete a user (prevents self-delete and last-admin delete). |
| POST | /api/admin/users/:userId/password | Set a user's password; invalidates all of their sessions. |
| POST | /api/admin/users/:userId/purge | Purge a user's authored content while keeping the account and its status. |
| PATCH | /api/admin/users/bulk-clearance | Toggle the cleared flag on multiple users. |
| PATCH | /api/admin/users/bulk-account-status | Set the account-status lifecycle value for multiple users. |
| POST | /api/admin/users/bulk-delete | Delete multiple users. |
| PATCH | /api/admin/users/bulk-status | Set emailVerified for multiple users. |
| GET | /api/admin/email-logs | Paginated log of outbound emails with summary counts by status. |
GET /api/admin/users?page=1&limit=25&search=octo
{
"users": [
{
"id": "u1",
"email": "octo@example.com",
"username": "octo",
"displayName": "Octo",
"avatar": null,
"bio": null,
"emailVerified": true,
"cleared": false,
"accountStatus": "active",
"customerStatus": "free",
"createdAt": "2025-01-01T00:00:00.000Z",
"isAdministrator": false
}
],
"pagination": { "total": 1024, "limit": 25, "offset": 0, "page": 1, "hasMore": true }
}
POST /api/admin/users
Content-Type: application/json
{
"email": "user@example.com",
"username": "user1",
"password": "min8chars",
"displayName": "User One",
"emailVerified": false,
"isAdministrator": false,
"customerStatus": "free"
}
Hashes the password, optionally marks emailVerified, optionally promotes to administrator, and adds the user to The Public. If the user is not pre-verified, a verification email is sent.
| Status | Condition |
|---|---|
| 400 | Missing email/username/password, password under 8 chars |
| 409 | Email or username already in use |
PUT /api/admin/users/u1
Content-Type: application/json
{ "emailVerified": true, "customerStatus": "subscriber:monthly", "accountStatus": "active", "isAdministrator": true }
Any subset of email, username, displayName, avatar, bio, emailVerified, cleared, accountStatus, customerStatus, isAdministrator may be supplied. Email/username uniqueness is re-checked. Promoting/demoting administrator updates the Administrator table.
accountStatus (see Account status) must be one of probation, active, restricted, suspended, banned. Setting it to active also sets cleared: true (unless cleared is sent explicitly in the same request). Setting it to banned deletes the target's sessions and sync tokens in the same transaction, forcing a full logout. Guards: an admin cannot change their own accountStatus, and cannot restrict/suspend/ban the last remaining administrator.
| Status | Condition |
|---|---|
| 400 | accountStatus not one of the five valid values |
| 400 | Attempted to change your own accountStatus, or to lock/ban the last administrator |
| 404 | User not found |
POST /api/admin/users/u1/password
Content-Type: application/json
{ "password": "min8chars" }
Sets the password and invalidates all of the target user's sessions in the same transaction.
Every user carries an accountStatus lifecycle value that can limit or block their write actions. The five values are probation (new signup, plain posts only, rate-limited), active (full access), restricted and suspended (read-only, can appeal), and banned (terminal, logged out). New signups start at probation; on email verification the account auto-transitions to active or restricted based on its spam score. Enforcement of these limits is server-side and gated by a deployment flag; when enforcement is on, blocked writes return the account-status error codes.
PATCH /api/admin/users/bulk-account-status
Content-Type: application/json
{ "userIds": ["u1", "u2"], "accountStatus": "restricted" }
Sets accountStatus for every listed user. Side effects mirror the single-user PUT:
active also sets cleared: true (not-spam semantics).banned deletes those users' sessions and sync tokens (force logout everywhere).Returns { "updated": N }.
| Status | Condition |
|---|---|
| 400 | userIds missing/empty, or accountStatus not one of probation, active, restricted, suspended, banned |
| 400 | The list includes yourself (you cannot change your own status) |
| 400 | The change would restrict/suspend/ban the last remaining administrator(s) |
POST /api/admin/users/u1/purge
Content-Type: application/json
{
"categories": { "messages": true, "directMessages": true, "documents": true, "lists": true },
"reason": "Confirmed spam account",
"dryRun": false
}
Deletes the target user's authored content while leaving the User row and its accountStatus intact, so a restricted / suspended / banned account keeps its lock after its content is removed. Each purge writes one row to the UserContentPurge audit table.
Body fields (all optional)
| Field | Type | Description |
|---|---|---|
categories | object | Any subset of messages, directMessages, documents, lists, each a boolean. Omitted or omitted-key defaults to true (that category is purged). An explicit false opts a category out. Omitting categories entirely purges all four. |
reason | string | Free-text reason recorded in the audit row. |
dryRun | boolean | When true, returns preview counts only and makes no writes. Defaults to false. |
messages: every Message the user authored (posts + replies), their digs/reports, external cross-posts, and message/video/DM-image blobs.directMessages: DirectMessage rows they sent (outbound only), plus DM-image blobs. Received DMs are left alone.documents: their Documents and Folders (with collaborators, share links, invites, presence) plus document blobs.lists: their Lists and ListFolders (with properties, rows, watchers, share links, invites).Dry-run response 200 OK
{ "dryRun": true, "counts": { "messages": 42, "directMessages": 3, "documents": 1, "folders": 0, "lists": 2, "listFolders": 0 } }
Purge response 200 OK
{ "dryRun": false, "counts": { "messages": 42, "directMessages": 3, "documents": 1, "folders": 0, "lists": 2, "listFolders": 0 } }
| Status | Condition |
|---|---|
| 400 | Attempted to purge your own content, an unknown category key, or a non-boolean category value |
| 404 | User not found |
The purge is irreversible. It runs in batches so a prolific account's content does not blow a single transaction, and it never touches analytics (the user still exists).
PATCH /api/admin/users/bulk-clearance
Content-Type: application/json
{ "userIds": ["u1", "u2", "u3"] }
Toggles the cleared flag for each user (records a cleared analytics action for every newly-cleared user). Returns { "updated": N }.
POST /api/admin/users/bulk-delete
Content-Type: application/json
{ "userIds": ["u1", "u2"] }
Deletes the listed users. The caller cannot include themselves; the request fails if it would remove every remaining administrator.
PATCH /api/admin/users/bulk-status
Content-Type: application/json
{ "userIds": ["u1", "u2"], "emailVerified": true }
Sets emailVerified to the given boolean for every user in the list.
GET /api/admin/email-logs?limit=25&status=delivered&dateRange=7d&sort=desc
| Query | Default | Notes |
|---|---|---|
limit | 25 | Clamped to 100. |
offset | 0 | |
status | none | Exact match. |
emailType | none | Exact match. |
search | none | Case-insensitive contains on recipient. |
dateRange | all | today, 7d, 30d, or all. |
sort | desc | asc or desc on createdAt. |
Response includes logs, total, summary (counts by status), and pagination.