Account
The account itself, and only with a session.
Nothing here takes a key. A key that could make another key would give itself every scope at once, and one that could ask for the export would be one leak away from the whole account. There is no scope that makes this safe, so there is none: session only.
A key that tries answers 403 forbidden.
/api/ v1/ account/ usernameSays whether a name is free, before anybody tries to take it.
Query
| Field | Type | Notes |
|---|---|---|
qrequired | string | The name to check. One that is not even shaped like a name answers available: false rather than erroring. |
/api/ v1/ account/ usernameTakes a name for the first time. Separate from the PATCH because they are different acts with different rules: an account without a name yet is finishing signing up, and one with a name is renaming.
Body
| Field | Type | Notes |
|---|---|---|
usernamerequired | string | 3 to 24 characters of a-z, 0-9 and _, starting and ending with neither _. |
/api/ v1/ account/ usernameRenames. There is a waiting period between one change and the next, and the answer carries next_change_at to say when it ends.
/api/ v1/ indexRecent public content for indexers. Up to 1000 items per kind: reviews, entries, lists and screenshots. RLS defines visibility.
/api/ v1/ account/ bootstrapPrepare the profile after sign-in. Session only. adopt_twitch=true adopts the existing Twitch identity.
/api/ v1/ account/ stateActive suspension, reason and infraction count for the caller, and infraction_history: every entry behind that count, newest first, with id, reason (the kind, stored in Portuguese: Aviso, Suspensão or Banimento permanente), details (moderation's own words, or null), expires_at and created_at. Session only.
Example response
{
"data": {
"suspended": false,
"state": null,
"infractions": 1,
"infraction_history": [
{
"id": "5c1d…",
"reason": "Aviso",
"details": "Spoilers sem aviso numa avaliação.",
"expires_at": null,
"created_at": "2026-09-14T18:02:11.204+00:00"
}
],
"username": "ada"
}
}/api/ v1/ account/ birth-dateReturns the stored date of birth and how the age was confirmed. Session only: the columns are revoked from everyone, and the definer function answers for the caller and nobody else.
/api/ v1/ account/ birth-dateStores the date of birth. The age rule is the database's, which refuses rather than storing a date that is too recent.
Body
| Field | Type | Notes |
|---|---|---|
birth_daterequired | string | A date as YYYY-MM-DD. |
/api/ v1/ account/ sessionsWhere the account is signed in, and on what.
/api/ v1/ account/ sessions/ {id}Ends one session. Including the one making the request, which is how you sign out of everywhere at once.
/api/ v1/ account/ identitiesThe providers this account can sign in with.
/api/ v1/ account/ exportEverything the account has written, in one document. A session that still owes its second factor gets 403 with reason mfa_required.
/api/ v1/ account/ dataThrows away one kind of thing, by name. The category is required and never defaults: a delete that guesses what it was asked to remove is a delete nobody takes back. It asks, in the JSON body, for proof that the owner of the account is the one asking now (see password and code): a session left open used to be enough to empty the account. Before any proof, a session that still owes its second factor gets 403 forbidden with reason mfa_required. It answers with how many records went, not a flat "done": saying data is gone when there was none to remove is a claim rather than a confirmation.
Query
| Field | Type | Notes |
|---|---|---|
categoryrequired | string | library, reviews, sessions, journeys, lists, screenshots, comments, views or everything. |
Body
| Field | Type | Notes |
|---|---|---|
password | string | The current password. Required, every time, for an account that has one. Without it, 403 forbidden with reason password_required; wrong, reason password_incorrect. |
code | string | For an account with no password and two-factor on: six digits from the authenticator app or a recovery code, instead of signing in again. Without a password, a session started in the last 15 minutes is enough on its own; after that, 403 forbidden with reason reauthentication_required and second_factor saying whether a code would do. Tries at a password or code: 10 every 15 minutes per account, then 429 rate_limited with retry_after. |
/api/ v1/ account/ keysThe account's keys. Never the token: only its hash is stored, in a column no role that talks to this API is granted.
/api/ v1/ account/ keysCreates a key on one of your apps, with the app's permissions. The token comes back once, in this answer, and never again: after that nobody can read it, not even us.
Body
| Field | Type | Notes |
|---|---|---|
app_idrequired | uuid | The key's app. Every key belongs to one, made in the developer portal. |
namerequired | string | Up to 60 characters. |
expires_in_days | integer | In how many days it expires. null for never. |
/api/ v1/ account/ connections/ {service}Forgets a linked account: twitch or steam. Only the undoing lives here. Linking one is an OAuth round trip that has to come back to a page, so it starts and ends in the browser; forgetting is a single write, and writes are here.
/api/ v1/ account/ keys/ {id}Revokes a key. It takes effect on the very next request.
/api/ v1/ account/ appsThe account's apps, most recently changed first, with how many live keys each has and when one was last used. arcade sums up, in one line, where the app's game stands on Arcade, or is null for an app without Arcade: slug, status, hidden_reason, notice (suspended, flagged, hidden or null), live_release_number, listing (approved, pending, draft and rejected), releases (draft, pending, ready and rejected_latest) and data_events (pending, held, which is part of pending, and failed). It is the same member in every app answer, and Arcade has the details at /api/v1/account/apps/{id}/arcade.
Example response
{
"data": [
{
"id": "...",
"name": "Orbit Runner",
"description": "",
"icon_url": null,
"scopes": [],
"created_at": "2026-10-14T18:00:00.000Z",
"updated_at": "2026-10-14T18:00:00.000Z",
"active_keys": 0,
"last_used_at": null,
"arcade": {
"slug": "orbit-runner",
"status": "listed",
"hidden_reason": null,
"notice": null,
"live_release_number": 3,
"listing": {
"approved": true,
"pending": null,
"draft": false,
"rejected": false
},
"releases": {
"draft": 1,
"pending": 0,
"ready": 0,
"rejected_latest": false
},
"data_events": { "pending": 0, "held": 0, "failed": 0 }
}
}
]
}/api/ v1/ account/ appsCreates an app, with no permissions. Up to 10 per account.
Body
| Field | Type | Notes |
|---|---|---|
namerequired | string | Up to 60 characters. |
description | string | Up to 400 characters. |
/api/ v1/ account/ apps/ {id}An app and its keys, the live ones first, with the list's arcade member (null without Arcade).
/api/ v1/ account/ apps/ {id}Changes the name, the description or the permissions. The permissions apply to every live key of the app too, from the very next request.
Body
| Field | Type | Notes |
|---|---|---|
name | string | Up to 60 characters. |
description | string | Up to 400 characters. |
scopes | array | The app's permissions, in full: any left out are taken away. |
/api/ v1/ account/ apps/ {id}Deletes the app and every key it holds, which stop working on the very next request. An app with a game on Arcade cannot go until the game is removed from Arcade: 409 conflict with reason remove_from_arcade_first.
/api/ v1/ account/ apps/ {id}/ keysAn app's keys. Never the token.
/api/ v1/ account/ apps/ {id}/ keysCreates a key for the app, with its permissions. The token comes back once, in this answer.
Body
| Field | Type | Notes |
|---|---|---|
namerequired | string | Up to 60 characters. |
expires_in_days | integer | In how many days it expires, from 1 to 3650. null for never. |
/api/ v1/ account/ apps/ {id}/ iconReplaces the icon: the image as multipart/form-data, in the image field (JPEG, PNG, WebP, GIF or AVIF, up to 15 MB). It is treated as an avatar: shrunk to fit 512x512 (the portal shows it cut square), at most 500 KB still or 4 MB and 15 seconds animated, and put through the same content check. Answers id and icon_url. A file of another type or over 15 MB, or an image the content check refuses, is 400 invalid_request; so is an image that could not be processed, and then with a reason: invalid_image (it could not be read), invalid_format (the content is not the type sent), too_many_frames (over 300 frames), too_many_pixels (over 40 million pixels, counting every frame), animation_too_long (over 15 seconds), unsupported_animation (an animated PNG or AVIF, or an animation without each frame's duration) or too_heavy (still over 500 KB, or 4 MB animated, after optimization). 404 when the app is not yours.
/api/ v1/ account/ apps/ {id}/ iconRemoves the icon; the app shows its initials again.
Proving it is you
Clearing data (DELETE /api/v1/account/data) cannot be taken back, so a
session alone is not enough for it: somebody who finds a laptop left signed in
has a session. The request carries a JSON body with proof that the owner is
the one asking now:
- An account with a password sends it as
password, every time. - An account without one (it signs in with Google, Discord, Twitch or a
passkey) needs nothing more if its session started in the last 15 minutes.
After that it signs in again, or, with two-factor on, sends
code: six digits from the authenticator app or a recovery code.
A refusal is 403 forbidden with a reason to act on:
{
"error": {
"code": "forbidden",
"message": "Sign in again to do this: the session has to have started in the last 15 minutes.",
"reason": "reauthentication_required",
"second_factor": true
}
}reason is one of password_required, password_incorrect,
reauthentication_required (with second_factor, whether a code would do)
and code_incorrect. Each account gets 10 tries at a password or a code every
15 minutes; past that the answer is 429 rate_limited with reason
too_many_attempts and retry_after in seconds.
Before any of that, a session that still owes its second factor (the account
has two-factor on and the code was not entered at sign-in) gets
403 forbidden with reason mfa_required, whatever the body carries. The
way through is finishing the two-factor step of signing in; a code in the
body does not stand in for it.
An old session cannot make itself new to get past this. For an account
without a password, the sign-in endpoints that would hand it a fresh session,
a second factor or a passkey (turning two-factor on or off, finishing its
setup, reading the authenticator secret, making recovery codes, registering a
passkey) refuse once the session is older than those 15 minutes, and the way
through is the same: sign in again. They live under /api/auth, the site's
sign-in service, and answer 403 in its shape:
{
"code": "RECENT_SIGN_IN_REQUIRED",
"message": "Sign in again to do this: the session has to have started in the last 15 minutes."
}Setting a first password (POST /api/account/password, the site's route for
it) follows the same rule, because a password is also a proof that outlives
the session: past those 15 minutes the answer is 403 with
{ "error": "reauthentication_required", "second_factor": false }.
An account with a password is unaffected: its proof is the password, and no new session stands in for it.