API reference
Every route the API serves, grouped by what it's for, with the parameters that matter and the error codes each can return.
Conventions
- Base URL: wherever the API runs (
http://localhost:5161 with make run). Member routes are under /userfeed,
/follows, /favourites and /media; staff routes under /admin and /ranking.
- Auth: every route needs
Authorization: Bearer <token> (in Development, or X-Dev-User: <id>). Without one:
401 unauthorized. Routes marked moderator, ranking admin or staff also need that role outside Development:
403 forbidden without it. See Sign-in and users.
- JSON in and out, camelCase,
Content-Type: application/json on bodies (415 unsupported_media_type
otherwise). IDs are UUID strings; times are ISO 8601 UTC.
- Paging:
limit (1–100, default 20) and cursor, the previous page’s nextCursor; null means the end.
- Every write by a suspended member is
403 user.suspended, except the reading signals.
- Every member route can return
404 user.not_synced if the caller’s user doesn’t exist (only when
Auth:ProvisionUsers is off, or a first-request race), and 400 validation_failed for a malformed ID, body or
out-of-range parameter. These aren’t repeated below.
- Errors are Problem Details with a stable
code: see Errors.
The OpenAPI document
In Development the API serves its OpenAPI document at /openapi/v1.json
(http://localhost:5161/openapi/v1.json); import it into Postman, Bruno or
a client generator. The admin panel’s API reference page (System → API reference) shows a saved copy, for environments
where the live document isn’t served. These tables add what the document can’t say: what each route is for, and the
error codes it returns.
Feeds
|
Route |
Purpose |
Query |
Errors |
| GET |
/userfeed/home |
Following: posts from accounts you follow and your own, newest first |
limit, cursor (a post ID) |
|
| GET |
/userfeed/foryou |
For You: ranked, each post with ranking |
limit, cursor (an offset), profile |
404 profile.not_found |
| GET |
/userfeed/favourites |
Posts from your favourites, newest first |
limit, cursor |
|
| GET |
/userfeed/tags/trending |
Tags on the most recent posts, hashtags and inferred topics alike: [{ tag, posts }] |
hours (1–168, 24), limit (1–50, 10) |
|
| GET |
/userfeed/tags/{tag}/posts |
Posts with a tag, newest first |
limit, cursor |
|
| GET |
/userfeed/mentions |
Posts and comments that @mention you, newest first: { mentions, nextCursor } |
limit, cursor |
|
| GET |
/userfeed |
Your own posts, in the bare post shape |
limit, cursor, order (newest/oldest), gt, gte, lt, lte (post IDs) |
|
Feed responses are { posts, nextCursor, source } (Following, favourites, tags) or
{ posts, nextCursor, ranker, profile, candidates } (For You). See Feeds and paging and the
types.
Posts
|
Route |
Purpose |
Body |
Success |
Errors |
| POST |
/userfeed/posts |
Create a post |
{ content, mediaIds? } |
201 post |
422 content.violation, 400 post.too_many_media, 404 media.not_found, 409 media.not_ready |
| PUT |
/userfeed/posts/{id} |
Edit the text, within 60 minutes |
{ content } |
200 post |
404 post.not_found, 403 post.not_author, 409 post.edit_window_closed, 409 post.is_repost, 422 content.violation |
| DELETE |
/userfeed/posts/{id} |
Delete your post |
|
204 |
404 post.not_found, 403 post.not_author |
The post is { id, authorId, content, createdAt, editedAt, repostOfId, isHeld }. There’s no route to fetch one post.
Engagement
|
Route |
Purpose |
Success |
Errors |
| PUT |
/userfeed/posts/{id}/like |
Like; repeating is a no-op |
204 |
404 post.not_found (also for a repost’s own ID) |
| DELETE |
/userfeed/posts/{id}/like |
Unlike; repeating is a no-op |
204 |
404 post.not_found |
| PUT |
/userfeed/posts/{id}/repost |
Repost (the original or a repost of it) |
204 |
404 post.not_found |
| DELETE |
/userfeed/posts/{id}/repost |
Undo your repost |
204 |
404 post.not_found |
| PUT |
/follows/{externalUserId} |
Follow; repeating is a no-op |
204 |
404 user.not_found, 400 follow.self |
| DELETE |
/follows/{externalUserId} |
Unfollow |
204 |
404 user.not_found, 400 follow.self |
| GET |
/favourites |
Your favourites, newest first: [{ externalUserId, username, displayName, avatarUrl, addedAt }] |
200 |
|
| PUT |
/favourites/{externalUserId} |
Add a favourite (up to 30) |
204 |
404 user.not_found, 400 favourite.self, 409 favourite.limit_reached |
| DELETE |
/favourites/{externalUserId} |
Remove a favourite |
204 |
404 user.not_found, 400 favourite.self |
|
Route |
Purpose |
Body / query |
Success |
Errors |
| GET |
/userfeed/posts/{id}/comments |
Top-level comments, oldest first |
limit, cursor |
200 { comments, nextCursor } |
404 post.not_found |
| GET |
/userfeed/comments/{id}/replies |
Direct replies to a comment |
limit, cursor |
200 { comments, nextCursor } |
404 comment.not_found |
| POST |
/userfeed/posts/{id}/comments |
Comment, or reply with parentCommentId |
{ content, parentCommentId? } |
201 comment |
404 post.not_found, 404 comment.not_found (parent), 422 content.violation |
| DELETE |
/userfeed/comments/{id} |
Delete your comment |
|
204 |
404 comment.not_found, 403 comment.not_author |
Reports
|
Route |
Body |
Success |
Errors |
| POST |
/userfeed/posts/{id}/report |
{ reason }: spam, harassment, hate, threat, sexual, self_harm or other |
204 |
404 post.not_found (also for reposts) |
| POST |
/userfeed/comments/{id}/report |
{ reason } |
204 |
404 comment.not_found |
Reporting twice counts once; three different reporters hold the item for review
(Moderation).
Reading signals
Accepted from suspended members too. See Reading signals.
|
Route |
Purpose |
Body |
Success |
Errors |
| POST |
/userfeed/impressions |
Posts seen: at least half on screen for a second |
{ postIds } (up to 100) |
202 { counted } |
|
| POST |
/userfeed/dwell |
Time on screen per view, sent as each scrolls away (60 s cap per view) |
{ views: [{ postId, ms }] } (up to 100) |
202 { counted } |
|
| POST |
/userfeed/posts/{id}/open |
The member opened the post |
|
202 { counted } |
|
| POST |
/userfeed/posts/{id}/not-interested |
Hide from For You for 30 days; less of its author and topics |
|
204 |
404 post.not_found |
| POST |
/userfeed/posts/{id}/show-more |
More of its author and topics |
|
204 |
404 post.not_found |
Photos
|
Route |
Purpose |
Body |
Success |
Errors |
| POST |
/media/uploads |
An upload slot and signed URL |
{ contentType, size } |
201 { mediaId, uploadUrl, contentType, expiresAt } |
415 media.type_not_allowed, 413 media.too_large |
| POST |
/media/{id}/complete |
The file is uploaded; start the checks |
|
202 media |
404 media.not_found, 409 media.wrong_state |
| GET |
/media/{id} |
Status of your upload: awaitingUpload, processing, ready or rejected |
|
200 media |
404 media.not_found |
| PUT |
/blobs/{key}?expires=&sig=&contentType= |
Development only: the signed upload URL itself (local storage) |
the file |
200 |
403 bad signature or type, 413 too large |
| GET |
/blobs/{key}?expires=&sig= |
Development only: a signed read URL |
|
200 the file |
403, 404 |
The /blobs routes exist only with BlobStorage:Provider Local; you never build their URLs yourself, the API
hands them out. See Photos and Composing with photos.
Ranking profiles (ranking admin)
|
Route |
Purpose |
Errors |
| GET |
/ranking/profiles |
Presets first, then saved profiles at their latest version |
|
| GET |
/ranking/profiles/{key} |
One profile: { key, version, preset, label, isDefault, settings } |
404 profile.not_found |
| GET |
/ranking/profiles/{key}/versions |
A saved profile’s versions (empty for a preset) |
404 profile.not_found |
| GET |
/ranking/profiles/{key}/versions/{n} |
One version |
404 profile.not_found |
| PUT |
/ranking/profiles/{key} |
Create, or save a new version; body is the settings |
409 profile.read_only, 400 profile.invalid_key, 400 profile.invalid |
| DELETE |
/ranking/profiles/{key} |
Delete a saved profile |
409 profile.read_only, 404 profile.not_found |
| POST |
/ranking/preview?viewer=&limit= |
The For You these settings would give viewer (default you; limit up to 50), without saving or logging |
400 profile.invalid, 404 user.not_found |
| GET |
/ranking/vocabulary |
Variables, functions, operators, goals, presets and defaults, for editors |
|
See Ranking profiles.
Moderation (moderator)
|
Route |
Purpose |
Errors |
| GET |
/admin/moderation/overview |
What’s waiting; the last day’s blocks, holds, reports and decisions |
|
| GET |
/admin/moderation/queue |
Held and flagged posts and comments, oldest first |
|
| GET |
/admin/moderation/log?action=&limit= |
Latest events; action blocked, held, reported or reviewed; limit 1–500 (100) |
|
| GET |
/admin/moderation/reports?limit= |
Latest member reports, newest first; limit 1–500 (100) |
|
| POST |
/admin/moderation/posts/{id}/decision |
{ "decision": "approved" | "removed" } |
404 post.not_found |
| POST |
/admin/moderation/comments/{id}/decision |
{ "decision": "approved" | "removed" } |
404 comment.not_found |
| GET |
/admin/users?q=&status=&sort=&offset=&limit= |
Search users; status active/suspended; sort username, posts, reports or flags |
|
| GET |
/admin/users/{externalUserId} |
A user for staff |
404 user.not_found |
| PUT |
/admin/users/{externalUserId}/status |
{ "status": "suspended" | "active" }, recorded in the log |
404 user.not_found |
| PUT |
/admin/users/{externalUserId}/verified |
{ "verified": true } |
404 user.not_found |
| GET |
/admin/posts?q=&author=&status=&reported=&offset=&limit= |
Posts in every state; status visible, held, removed or deleted |
|
| GET |
/admin/posts/{id} |
A post with its comments, reports and moderation history |
404 post.not_found |
| GET |
/admin/media/{id} |
An upload with its nudity score and the line it’s judged against |
404 media.not_found |
Staff (moderator or ranking admin)
|
Route |
Purpose |
| GET |
/admin/me |
Any signed-in user: who they are and which roles they have (moderator, rankingAdmin, environment) |
| GET |
/admin/dashboard?minutes=60 |
Everything the admin panel’s overview shows, in one call |
| GET |
/admin/system |
Sign-in settings (no secrets), moderation thresholds, media limits, models, ranker, users by status, outbox and consumers |
| GET |
/admin/feed/{externalUserId}/following |
A member’s Following as they’d see it, without logging it |
| GET |
/admin/feed/{externalUserId}/foryou?profile= |
A member’s For You in a session of its own, without logging it |
Other
|
Route |
Purpose |
| GET |
/me |
The caller’s externalUserId; a quick token check |
| GET |
/openapi/v1.json |
The OpenAPI document, Development only |
Edit this page on GitHub