The WC26 Threads API.
Base URL
All endpoints are served over HTTPS under the /api path:
A simple health check lives at the API root — it returns service status and the running build version.
→ 200 { "status": "ok", "version": "1.0.0" }
Authentication
The API uses stateless JWT authentication. There are no sessions on the server — every authenticated request carries its own token.
The flow is three steps:
- Register or log in — both return a signed JWT.
- Store the token client-side (the Android app uses encrypted DataStore).
- Send it on protected requests via the
Authorizationheader.
POST /api/auth/register
# 2. Use the token on protected routes
Authorization: Bearer <your-jwt>
Many read endpoints support optional authentication: they
work without a token, but return richer data when one is supplied. For
example, the posts feed includes a likedByCurrentUser flag on
each post only when the request is authenticated.
Pagination
The API uses two pagination strategies, each suited to its data.
Offset pagination — for bounded datasets like matches and
comments. Pass limit and offset query parameters.
Responses include a total count.
Cursor pagination — for unbounded, infinite-scroll feeds
like posts and likes. The response carries a nextCursor string;
pass it back as the cursor parameter to fetch the next page.
When nextCursor is null, there are no more results.
→ { "items": [...], "nextCursor": "MjAyNi0wNS0xMHwxNQ==" }
GET /api/posts?limit=20&cursor=MjAyNi0wNS0xMHwxNQ==
Errors
Errors return the appropriate HTTP status code with a JSON body containing
an error field describing the problem.
Common status codes across the API:
Matches
All 104 World Cup matches, seeded into the database. Read-only for regular users; scores and status are updated by admins.
List matches, paginated. Filterable by status and stage.
limit, offset, status (scheduled / live / finished), stage total count Fetch a single match by its ID.
Auth
Registration, login, and the current-user lookup. Register and login return a JWT valid for 24 hours.
Create an account. Returns a JWT and the new user.
email, username, password, displayName {
"email": "fan@example.com",
"username": "supporter",
"password": "a-strong-password",
"displayName": "A Fan"
}
Exchange email and password for a JWT.
email, password Return the full profile of the authenticated user, including email and role.
Users
Public user profiles. Email and role are never exposed here — only
through /auth/me for the user themselves.
Public profile: id, username, display name, avatar, join date.
A user's posts, newest first, cursor-paginated. With auth, each post carries likedByCurrentUser.
limit, cursor Posts a user has liked, cursor-paginated. The likedByCurrentUser flag reflects the viewer's likes, not the profile owner's.
Posts
Posts are takes attached to a match. Each post belongs to one match and one author, and carries denormalized like and comment counts.
Global feed of all posts, newest first, cursor-paginated.
limit, cursor Posts for one match — the match thread. Cursor-paginated, newest first.
A single post with its author and counts.
Create a post on a match.
content — 1 to 500 characters Delete a post. Only the author may delete their own post.
Likes
Liking is idempotent — liking an already-liked post is a no-op, not an
error. The post's likeCount is kept accurate with atomic
counter updates.
Like a post. Idempotent — returns 204 whether or not the post was already liked.
Remove a like. Idempotent — returns 204 even if the post was not liked.
List the users who liked a post, cursor-paginated.
Comments
Comments are replies on a post, newest first. Posts carry a denormalized
commentCount.
List comments on a post, newest first, offset-paginated.
limit, offset Add a comment to a post.
content — 1 to 300 characters Delete a comment. Only the comment's author may delete it.
Admin
Admin-only endpoints. These require a JWT whose role claim is
admin — enforced by a dedicated authentication provider,
not an inline check.
Update a match's score and / or status. Partial update — send only the fields to change.
homeScore, awayScore, status — all optional