Endpoints
All paths are relative to https://www.remilia.net/api/v1. Responses are JSON. Send Authorization: Bearer <access_token> on any endpoint requiring a scope (see Login Quickstart and API Quickstart).
Every endpoint except the two profile reads wraps its success payload in a data envelope:
{ "data": { "...": "endpoint payload" } }Errors always use the error envelope, regardless of endpoint.
Profiles
Get a user profile
GET /users/{username}Returns the public profile for {username}.
Scope: none — public.
A token is optional. An expired or malformed token returns 401 invalid_token (tokens are validated even here). A valid app-only token receives the same anonymous view as a tokenless request; a user-delegated token personalizes viewerContext and the isAuthenticated / isOwnProfile flags.
curl https://www.remilia.net/api/v1/users/remiliaThe profile object is returned directly (no data wrapper):
{
"user": {
"username": "remilia",
"displayName": "Remilia",
"bio": "...",
"location": "...",
"pfpUrl": "...",
"friendCount": 0,
"achievementsCount": 0
},
"achievementContext": {},
"viewerContext": { "areFriends": false },
"isAuthenticated": false,
"isOwnProfile": false
}An unknown username returns 404 with the error envelope:
{ "error": { "code": "not_found", "message": "user not found" } }Get your own profile
GET /meReturns the profile of the token's user — the same shape as GET /users/{username} for your own username, with isOwnProfile: true and the owner-only fields (user.stats, user.aggregateScores, user.property) populated.
Scope: none · user-delegated token required — a bare openid grant works.
curl https://www.remilia.net/api/v1/me \
-H "Authorization: Bearer $ACCESS_TOKEN"Like the public profile read, the object is returned bare (no data wrapper). An app-only token returns 403 requires_user.
Get your account statistics
GET /me/statsReturns per-platform statistics and aggregate scores for the token's user.
Scope: remilia:stats.read · user-delegated token required
curl https://www.remilia.net/api/v1/me/stats \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"data": {
"handle": "remilia",
"display_name": "Remilia",
"stats": {
"miladychan": { "...": "..." },
"twitter": { "...": "..." },
"profiles": { "...": "..." },
"beetle_game": { "...": "..." },
"miladycraft": { "...": "..." },
"ethereum": { "cult_tier": "...", "total_owned": 0, "collections": {} },
"merch": { "...": "..." }
},
"aggregate_scores": { "...": 0 }
}
}Notifications
List your notifications
GET /me/notificationsScope: remilia:notifications.read · user-delegated token required
| Query parameter | Meaning |
|---|---|
limit | Page size. Default 10; values outside [1, 50] fall back to the default. |
page | 1-based page number. Default 1. |
curl "https://www.remilia.net/api/v1/me/notifications?limit=20&page=1" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"data": {
"notifications": [
{
"type": "poke",
"read": false,
"from": {
"handle": "milady",
"display_name": "Milady",
"avatar_url": "..."
},
"created_at": 1754870400
}
],
"page": 1,
"limit": 20,
"has_more": false
}
}count, from, guild_name, and message appear only on notification types that carry them.
Beetle
Get your Beetle state
GET /me/beetleReturns the caller's Beetle game state — XP, level, inventory, hammer durabilities, and cooldowns — wrapped in data.
Scope: remilia:beetle.read · user-delegated token required
List your discovered Beetle cards
GET /me/beetle/cardsReturns the caller's discovered cards keyed by card codename, wrapped in data.
Scope: remilia:beetle.read · user-delegated token required
Pokes
Poke a user
POST /users/{handle}/pokeSends a poke to {handle}, attributed to the authenticated user.
Scope: remilia:pokes.write · user-delegated token required
curl -X POST https://www.remilia.net/api/v1/users/remilia/poke \
-H "Authorization: Bearer $ACCESS_TOKEN"Success:
{ "data": { "target_handle": "remilia", "poked_at": 1754870400 } }Poking the same user again within 24 hours returns 429 poke_cooldown:
{
"error": {
"code": "poke_cooldown",
"message": "poke on cooldown (23h59m30s remaining)",
"next_allowed_at": 1754956800
}
}Global chat
List global chat messages
GET /global-chat/messagesReturns global chat messages, oldest first.
Scope: remilia:chat.read · user-delegated or app-only token
| Query parameter | Meaning |
|---|---|
limit | Page size. Default 20, capped at 100. Must be a positive integer. |
before | A message id; returns messages older than it. Omit for the newest page. |
curl "https://www.remilia.net/api/v1/global-chat/messages?limit=50" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"data": {
"messages": [
{
"id": "1042",
"author": {
"handle": "milady",
"display_name": "Milady",
"avatar_url": "..."
},
"text": "gm",
"created_at": 1754870400
}
],
"has_more": true,
"next_cursor": "998"
}
}When has_more is true, pass next_cursor as before to fetch the next-older page. Messages may also carry reply_to_id, media (kind, url, thumbnail_url, width, height, is_animated), reactions (emoji, count), and edited_at.
Post a global chat message
POST /global-chat/messagesPosts a text message to global chat as the authenticated user.
Scope: remilia:chat.write · user-delegated token required
curl -X POST https://www.remilia.net/api/v1/global-chat/messages \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "gm", "reply_to_id": "1042"}'text is required, up to 8192 bytes. reply_to_id is optional.
Returns 201 with the created message (same shape as the list) in data.
| Status | error.code | When |
|---|---|---|
| 400 | invalid_request | Missing or over-long text, malformed body, or a non-numeric reply_to_id |
| 400 | invalid_reply | reply_to_id does not reference a real message |
| 403 | forbidden | You are muted or not permitted to post to global chat |
| 413 | invalid_request | Request body too large |
| 429 | rate_limited | Posting faster than one message every 2 seconds |
Directory
Resolve directory identities
POST /directory/resolveResolves up to 50 tagged identifiers to narrow public identities in one batch. Results preserve request order; each result echoes its query, with identity set to null for a miss.
Scope: remilia:directory.read · user-delegated or app-only token
Identifier type is one of handle, x_handle, github, discord.
curl -X POST https://www.remilia.net/api/v1/directory/resolve \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"identifiers": [{"type": "handle", "value": "remilia"}]}'{
"data": {
"results": [
{
"query": { "type": "handle", "value": "remilia" },
"identity": {
"handle": "remilia",
"display_name": "Remilia",
"avatar_url": "...",
"avatar_url_small": "..."
}
}
]
}
}Enumerate the user directory
GET /usersPages through all directory-visible users in a stable order, using cursor pagination.
Scope: remilia:directory.read · user-delegated or app-only token
| Query parameter | Meaning |
|---|---|
limit | Page size. Default 25, capped at 100. Must be a positive integer. |
cursor | Opaque continuation token from the previous page. Omit for the first page. |
curl "https://www.remilia.net/api/v1/users?limit=100" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"data": {
"users": [
{
"handle": "remilia",
"display_name": "Remilia",
"avatar_url": "...",
"avatar_url_small": "..."
}
],
"next_cursor": "MTA0Mg"
}
}next_cursor is null on the final page; otherwise pass it back verbatim as cursor.
Errors
Errors use the envelope:
{ "error": { "code": "...", "message": "..." } }| Status | error.code | When |
|---|---|---|
| 400 | invalid_request | A malformed query parameter or request body |
| 401 | unauthorized | No token on an endpoint that requires one |
| 401 | invalid_token | The bearer token is invalid or expired — sent even on public endpoints |
| 403 | insufficient_scope | The token lacks the scope the endpoint requires |
| 403 | requires_user | The endpoint acts for a user but the token is app-only (API client) |
| 403 | forbidden | The account is disabled, or the action is not permitted |
| 404 | not_found | No such endpoint, or no such user profile |
| 404 | user_not_found | The target user of an action does not exist |
| 405 | method_not_allowed | Wrong HTTP method for the path |
| 429 | rate_limited, poke_cooldown | Sending too fast; poke_cooldown includes next_allowed_at |
| 500 | internal_error | Something failed on our side — safe to retry with backoff |
Endpoint-specific codes (invalid_reply, poke_cooldown) are listed with their endpoints above.