Skip to content

Auth and Accounts

leanwrldd edited this page Sep 10, 2026 · 4 revisions

← Back to Home

Auth & Accounts

Roles

Role role string is_editor is_administrator is_contributor is_manager
Standard applicant — — — —
Editor editor ✓ — — —
Contributor contributor — — ✓ —
Manager manager — — — ✓
Admin administrator ✓ ✓ — —

New Discord users start as applicant. Editors are promoted after application approval. Admins are assigned manually. Admins always have is_editor: true.

contributor is a separate track from editor — it grants access to the comp-file proposal pipeline below (uploading/replacing/moving/deleting files in the compilation), not to song-data edit proposals. manager is likewise independent — a flag (manager_enabled on /admin/users/, is_manager on the account payload) that grants the proposal/comp-proposal review queues without full admin access (no user management, no security settings). A user can hold any combination of editor/contributor/manager. All three are optional/undefined on older account payloads — read them defensively.

is_news is a fourth, similarly independent flag — it grants News write access (create posts, edit/delete your own) without needing is_editor. Admins get News write access regardless of is_news. See the News page.

On a deployment with multiple comp channels (see Comp Channels on the Files & Stream page), these role booleans are the global grant. A user can additionally hold editor/contributor/manager per comp channel via account.memberships — an array of { channel_slug, channel_name, is_primary, is_editor, is_contributor, is_manager, auto_approve_proposals, auto_approve_comp_proposals } rows, one per channel the user has any flag set on. Admins implicitly get every active channel with every flag on. Treat a missing global flag as "check memberships for this channel" rather than "denied." This is a completely separate system from News channels — different slugs, different roles, different storage.

Attach token to every authenticated request:

Authorization: Token YOUR_TOKEN_HERE

Discord Login (recommended)

  1. GET /accounts/auth/discord/url/ → returns authorize_url and state
  2. Redirect the user through Discord OAuth using authorize_url
  3. Exchange the code Discord returns:
POST /accounts/auth/discord/exchange/

{
  "code": "discord_auth_code",
  "state": "state_from_step_1",
  "redirect_uri": "https://your-app.com/callback"
}

// Response includes token + user object

Store the token and attach it as Authorization: Token <token> on subsequent requests.

GET /accounts/discord/login/ and POST /accounts/discord/callback/ are an older, fixed-redirect variant of the same flow — no redirect_uri param on the URL step, same response shapes otherwise. Prefer the two-step flow above for a new integration.

Access Code Login

A simple shared-password gate, unrelated to Discord OAuth or the token/role system. Used to lock an otherwise-public deployment behind one access code.

POST /juicewrld/auth/login/

{ "password": "required" }

200:

{ "success": true, "message": "Authentication successful" }

401:

{ "success": false, "message": "Invalid access code" }

Logout

POST /accounts/logout/
Authorization: Token <token>

Invalidates the token server-side. Clear the locally stored token regardless of whether this call succeeds.

Admin Login

Username/password — administrators only. Not needed for a public music player.

POST /accounts/login/

{
  "token": "abc123...",
  "profile": { "role": "administrator", "is_editor": true, "is_administrator": true },
  "requires_otp_setup": false
}

Who Am I — Two Endpoints

GET /accounts/account/me/

Public-facing. No role string — booleans only. Use this for music player UI gating.

{
  "id": 42,
  "display_name": "someuser",
  "discord_id": "123456789",
  "discord_username": "someuser",
  "discord_avatar": "https://cdn.discordapp.com/avatars/...",
  "is_editor": false,
  "is_administrator": false,
  "is_contributor": false,
  "is_manager": false,
  "is_news": false,
  "otp_enabled": false,
  "user_preferences": [
    { "song": 94086, "name": "My title", "cover_url": "/assets/wod.jpg", "default_version": "v1", "playcount": 12 }
  ],
  "playlist_folders": [
    { "id": "f1", "name": "Favorites", "playlist_ids": [12, 34] }
  ],
  "listening_plays": [
    { "song": 94086, "played_at": "2026-08-03T20:14:00Z" }
  ],
  "news_subscriptions": ["announcements"],
  "memberships": [
    { "channel_slug": "alrdywrld", "channel_name": "alrdywrld", "is_primary": false, "is_editor": true, "is_contributor": true, "is_manager": false, "auto_approve_proposals": false, "auto_approve_comp_proposals": false }
  ]
}

Also mounted at /juicewrld/accounts/account/me/ (same handler, different prefix).

listening_plays, is_manager, is_news, news_subscriptions, and memberships aren't guaranteed present on every account payload yet — read them defensively (optional/undefined, not required). memberships only lists channels the user has at least one flag set on; see Roles above for how per-channel access composes with the global booleans. news_subscriptions is capped at 50 entries (news-channel slugs the user follows).

PATCH /accounts/account/me/

Updates the logged-in user's own user_preferences, playlist_folders, and/or listening_plays blobs — see the sections below for what goes in each.

GET /accounts/me/

Editor/admin dashboards. Includes raw role string and extra stats.

{
  "username": "discord_123456789",
  "role": "applicant",
  "is_editor": false,
  "is_administrator": false,
  "is_contributor": false,
  "is_superuser": false,
  "otp_enabled": false,
  "discord_id": "123456789",
  "discord_username": "someuser",
  "discord_avatar": "https://cdn.discordapp.com/avatars/...",
  "approved_count": 0,
  "badges": []
}

role is "applicant" | "editor" | "contributor" | "administrator" — widened from three values to four with the contributor track. Don't treat it as a strict editor-vs-admin ladder; check the specific boolean (is_editor/is_contributor/is_administrator) for the access you actually need.

Per-Song Preferences — custom titles, covers, playcounts

user_preferences is a per-user, per-song override list carried on the profile: a personal display name, a personal cover, a preferred version to play within the song's version group, and a playcount. These are personal only — they never change the song for anyone else, and editors proposing upstream edits see the API's own untouched values.

{
  "song": 94086,              // API song id (song.id, not public_id)
  "name": "My title",         // null = use the song's own title
  "cover_url": "Compilation/.../cover.jpg",
  "default_version": "v1",    // null = no preference
  "playcount": 12
}
Field Type Meaning
song number Which song this row overrides. The only required field.
name string | null Custom display title. Null falls back to the song's own title.
cover_url string | null Custom cover art. Null falls back to the song's image_url.
default_version string | null Preferred version label (e.g. v1, OG, TV Mix) — matched against the version field in the /versions/ table, not a song id, so it survives songs being relinked or groups merging. A default set on any group member governs the whole group.
playcount number How many times this user played the song. Client-owned — there is no server-side increment endpoint.

Resolving cover_url:

Form Example Resolves to
Absolute URL https://… / data: / blob: Used as-is
Leading slash /assets/wod.jpg Site-relative asset — prepend https://juicewrldapi.com (same shape as a song's image_url)
Anything else Compilation/…/cover.jpg A path into file storage — fetch via /files/cover-art/?path=

Write semantics — read this before implementing:

  • It's one JSON blob, not per-song rows — there is no per-song save and no delete. The client owns the whole array and PATCHes it in full; sending a shorter array is how a row gets removed.
  • Debounce the pushes. A burst of edits (or plays) should collapse into one PATCH rather than one per change.
  • On login, merge the server's copy into the local one taking max() of each playcount — otherwise plays made while signed out or on another device get overwritten.
  • The validator caps the array at 500 rows. Past that, drop playcount-only rows first — rows carrying a real override (name/cover/version) are the ones worth keeping, since every song played creates a playcount row.
  • Normalize cleared text fields to null, not "" — an empty string reads as a real override downstream.

Playlist Folders

playlist_folders groups the user's playlists into folders. Same blob mechanics as user_preferences above — whole-array PATCH on /accounts/account/me/, no per-folder route.

{
  "id": "f1",
  "name": "Favorites",
  "playlist_ids": [12, 34]
}
Field Type Meaning
id string Client-generated folder id — round-trips unchanged
name string Folder display name
playlist_ids number[] Library playlist IDs in this folder

Limits: max 200 folders, max 500 playlist ids per folder.

Listening History

listening_plays is a raw per-play log — one entry per listen, distinct from user_preferences[].playcount which is just a running total per song. Same blob mechanics: whole-array PATCH on /accounts/account/me/, no per-event route.

{
  "song": 94086,
  "played_at": "2026-08-03T20:14:00Z"
}
Field Type Meaning
song number API song id that was played
played_at string ISO 8601 timestamp of the play

Capped at 10,000 events — send the whole array, debounced the same way as user_preferences, so a burst of plays collapses into one PATCH rather than one per play.

Permission Matrix

Access Level Endpoints
No login Songs, eras, categories, files, radio, stats, shared playlists, play tracking, feedback submission, report submission
Any logged-in user /account/me/ (incl. PATCH), /application/, /library/*, GET /feedback/
Editor or admin /me/, /editor/proposals/, /editor/leaderboard/, /badges/, /reports/ (read + review)
Contributor /contributor/proposals/ (comp-file proposals — read/write your own)
Manager or admin /admin/proposals/, /admin/comp-proposals/ (review queues only — not /admin/users/ or /admin/applications/)
Admin only /admin/users/, /admin/applications/, /admin/comp-files/, /admin/channels/
is_news or admin, gated on author for edit/delete /news/ (is_news or admin can create; is_news holders can only edit/delete their own posts, admins any)
Beta code (X-Beta-Code) /beta/versions, /beta/download — independent of the token/role system

A 403 response means insufficient role — message text is typically "Editor access required." or "Administrator access required.".

Play tracking, feedback submission, song reports, and beta app access live on the Feedback & Reports page.

Edit Proposals (Editor+)

Method Path Description
GET /accounts/editor/proposals/ List the logged-in editor's own proposals
POST /accounts/editor/proposals/ Submit a new proposal
PATCH /accounts/editor/proposals/{id}/ Edit a still-pending proposal
DELETE /accounts/editor/proposals/{id}/ Withdraw a proposal

change_type is "create", "update", or "delete" — "update" is by far the most common in practice.

GET and POST both accept an optional channel param/field (slug) — omit for the primary channel's proposal queue. Channel access is governed by the per-channel is_editor membership flag (see Roles above), not just the global one.

POST /accounts/editor/proposals/
Authorization: Token <token>
Content-Type: application/json

{
  "change_type": "update",
  "song": 94086,
  "title": "Song Title",
  "editor_notes": "",
  "proposed_data": {
    "lyrics": "..."
  }
}
Field Type Description
change_type string "create" | "update" | "delete"
song number | null Internal song ID (song.id, not public_id) — null for a "create" proposal
title string Song title for display purposes
editor_notes string Optional notes from the editor
proposed_data object Only the fields being changed

To re-submit a stale/stuck pending proposal, DELETE it then POST the same data again as a fresh proposal (there's no separate "resubmit" endpoint).

Proposal object shape:

{
  "id": 167,
  "editor_username": "freakypallet",
  "editor_id": 12,
  "channel_slug": "compilation",
  "song": 94086,
  "song_public_id": 163,
  "change_type": "update",
  "title": "Song Title",
  "proposed_data": { "lyrics": "..." },
  "original_proposed_data": { "lyrics": "..." },
  "applied_data": {},
  "revised_by_admin": false,
  "original_snapshot": { /* Full song fields at time of proposal */ },
  "editor_notes": "",
  "status": "pending",
  "reviewer_username": null,
  "review_notes": "",
  "edit_count": 0,
  "last_edited_at": null,
  "created_at": "2026-06-16T22:07:24.970047Z",
  "reviewed_at": null
}

status is one of pending, approved, rejected, reversed.

Applications (Editor or Contributor)

How a standard user applies for either the editor or the contributor track — same endpoint, distinguished by application_type.

Method Path Description
GET /accounts/application/ Fetch the logged-in user's own application (null if none)
POST /accounts/application/ Submit an application
POST /accounts/application/

{
  "application_type": "editor",  // "editor" | "contributor"
  "display_name": "optional",
  "contact": "optional",
  "experience": "optional",
  "motivation": "required — why you want this access",
  "areas": "optional",
  "channel": "optional — applying for a role scoped to one channel rather than globally"
}

status on the returned application is pending, approved, or rejected.

Approving a contributor application should set is_contributor, not is_editor — the two tracks are separate (see Roles above).

Editor Leaderboard

GET /accounts/editor/leaderboard/

Ranked by approved proposal count. No auth required to view.

[
  {
    "rank": 1,
    "user_id": 12,
    "username": "freakypallet",
    "discord_username": "freakypallet",
    "discord_avatar": "https://cdn.discordapp.com/avatars/...",
    "approved_count": 214,
    "badges": [
      {
        "slug": "hundred-club",
        "name": "100 Club",
        "description": "100 approved edits",
        "icon": "🏅",
        "category": "milestone",
        "note": "",
        "awarded_at": "...",
        "awarded_by_username": null
      }
    ]
  }
]

Admin: User Lookup

Method Path Description
GET /accounts/admin/users/ List all users. Filter: ?role=editor|contributor|manager|administrator|applicant
GET /accounts/admin/users/{user_id}/ Single user detail — role, is_active, Discord info, proposal counts, badges
PATCH /accounts/admin/users/{user_id}/ Update role, is_active, auto_approve_proposals, or contributor/manager flags
PATCH /accounts/admin/users/{user_id}/

{
  "role": "contributor",            // "editor" | "contributor" | "manager" | "applicant"
  "is_active": true,
  "auto_approve_proposals": false,
  "contributor_enabled": true,
  "manager_enabled": false,
  "auto_approve_comp_proposals": false
}
Field Type Meaning
contributor_enabled boolean Whether this user has comp-file proposal access, independent of role string
manager_enabled boolean Whether this user can review proposals/comp-proposals without full admin access, independent of role string
auto_approve_comp_proposals boolean Skip manual review and apply this user's comp-file proposals automatically
comp_proposal_count number Read-only — this user's total comp-file proposal submissions
comp_approved_count number Read-only — how many of those were approved

These fields grant access globally. For a per-channel grant instead, use Admin: Channels below.

Requires admin token (is_administrator: true).

Admin: Channels

Manages the comp channels referenced throughout this doc (Files & Stream's ?channel=, proposal/comp-proposal scoping, per-channel memberships) — not News channels, a separate system. Exactly one channel has is_primary: true; it can never be deactivated and its is_active can't be changed.

Method Path Description
GET /accounts/admin/channels/ List all channels, including inactive ones (adds id/is_active/sort_order over the public /files/channels/ list)
POST /accounts/admin/channels/ Create a channel and its disk folders
PATCH /accounts/admin/channels/{id}/ Update name, description, sort_order, is_active
DELETE /accounts/admin/channels/{id}/ Deactivate (not a hard delete — files aren't touched). Primary can't be deleted
GET /accounts/admin/channels/{id}/members/ List the channel's per-user role memberships
POST /accounts/admin/channels/{id}/members/ Create or update one user's editor/contributor/manager flags for this channel
POST /accounts/admin/channels/

{
  "name": "alrdywrld",
  "description": "optional",
  "slug": "optional",
  "root_dirname": "optional",
  "sort_order": 0
}

slug and the comp_<slug> disk root are generated from name when omitted. A new channel is never primary.

PATCH /accounts/admin/channels/{id}/

{
  "name": "optional",
  "description": "optional",
  "sort_order": "optional",
  "is_active": true             // reactivate a previously-deactivated channel — no-op/rejected on the primary channel
}
Field Type Meaning
id number Numeric id — used in the URL, not the slug
slug string Stable identifier used as the ?channel= query value elsewhere
is_primary boolean True on exactly one channel — the default when ?channel= is omitted
is_active boolean False after DELETE — deactivated channels are hidden, not erased

Set a member's flags:

POST /accounts/admin/channels/{id}/members/

{
  "user_id": 42,
  "editor_enabled": true,
  "contributor_enabled": true,
  "manager_enabled": false,
  "auto_approve_proposals": false,
  "auto_approve_comp_proposals": false
}

Adding a member with every flag omitted/false still creates the membership row (visible in the channel's member list) with no access yet — flags are toggled afterward. On the primary channel specifically, setting these flags also mirrors the old global profile flags (is_editor, etc).

Admins implicitly get every active channel with every role flag on in memberships (see Roles above) — everyone else only sees channels they were explicitly added to.

Requires admin token (is_administrator: true).

Admin: Proposal Review

Method Path Description
GET /accounts/admin/proposals/ List all proposals. Filter: ?status=pending|approved|rejected|reversed&channel=
POST /accounts/admin/proposals/{id}/review/ Approve, reject, or revise-and-approve a proposal
POST /accounts/admin/proposals/{id}/reverse/ Reverse a previously approved proposal
POST /accounts/admin/proposals/{id}/review/

{
  "action": "approve",          // "approve" | "reject" | "revise"
  "review_notes": "optional",
  "revised_data": { },          // only for action: "revise" — overrides proposed_data
  "channel": "optional — the proposal's channel slug"
}

Requires admin or manager token, and — on a channel-scoped deployment — the per-channel is_manager/is_editor membership flag for the proposal's own channel.

Admin: Applications

Method Path Description
GET /accounts/admin/applications/ List editor applications. Filter: ?status=pending|approved|rejected
POST /accounts/admin/applications/{id}/review/ Approve or reject an application
POST /accounts/admin/applications/{id}/review/

{
  "action": "approve",          // "approve" | "reject"
  "review_notes": "optional"
}

Approving promotes the applicant to the role matching the application's application_type — editor or contributor.

Comp File Proposals — Overview

A second, separate proposal pipeline from song-data Edit Proposals above — this one is for changes to the compilation's files themselves (uploading a new file, replacing one, moving/renaming a file or a whole folder, or deleting a file or folder), submitted by contributors and reviewed by admins or managers. Everything under /accounts/contributor/ requires is_contributor (globally or via a comp-channel membership); everything under /accounts/admin/comp-proposals/ requires admin or manager (again, globally or per-channel); /accounts/admin/comp-files/ requires is_administrator.

Comp-channel scoped: list, create, review, and history calls only see that channel's proposals and files — pass ?channel=/a channel field on every endpoint below, same as the Files & Stream page. Staging storage lives under comp_staging/<slug>/proposals/ per channel (the primary channel keeps the older unprefixed comp_staging/proposals/ path).

Tracker path sync (server-side, automatic): when a comp file's path changes, the API updates matching Song.path values without a separate call. A single-file move (renames count) updates that one Song.path; rename_folder/move_folder updates every affected file's Song.path by swapping the folder prefix. delete/delete_folder leave Song.path untouched since the files are archived, not destroyed — refetch song data after a folder move/rename is approved to pick up the already-updated paths.

Contributor: Comp File Proposals

Method Path Description
GET /accounts/contributor/proposals/ List the logged-in contributor's own comp-file proposals
POST /accounts/contributor/proposals/ Submit a new comp-file proposal — multipart
PATCH /accounts/contributor/proposals/{id}/ Edit a still-pending proposal — multipart
DELETE /accounts/contributor/proposals/{id}/ Withdraw a proposal

The two write endpoints send multipart/form-data, not JSON — the request carries the actual file being uploaded/replaced alongside the metadata fields. Send Authorization: Token <token> and deliberately omit Content-Type so the browser sets the multipart boundary itself — setting it manually breaks the boundary and the server can't parse the body.

POST /accounts/contributor/proposals/
Authorization: Token <token>
Content-Type: multipart/form-data; boundary=... (set automatically — do not set this header yourself)

FormData:
  change_type       "upload" | "replace" | "move" | "delete" | "create_folder"
                    | "rename_folder" | "move_folder" | "delete_folder"
  file_path         "Compilation/Unreleased/Song.mp3"     // target path; a folder path for the folder change types
  destination_path  "Compilation/Unreleased/New Name.mp3" // "move", "rename_folder", "move_folder" only
  contributor_notes "optional"
  file              <binary>                              // only for "upload"/"replace"
  channel           "optional — channel slug, defaults to the primary channel"

create_folder takes only file_path (the new folder's path, with no extension) — no file and no destination_path. On approval the empty folder is created under comp/, ready to be filled with upload proposals.

Folder change types

No file is attached for any of the three — sending file on a folder proposal is rejected with a file field error. file_path/destination_path are comp-root-relative folder paths, forward slashes, no leading slash.

change_type Use when destination_path
rename_folder Only the final folder segment changes; parent stays the same Required — must share the same parent as file_path
move_folder The folder moves under a different parent directory Required — must have a different parent than file_path
delete_folder Archive and remove every active file in the folder, then remove it Omit — not accepted

Both rename and move require the source folder to exist and contain at least one active indexed file, and the destination to not already exist. Using the wrong one of the two for a same-vs-different-parent change is a 400: destination_path: "Rename must keep the folder in the same parent directory." or "Move must place the folder under a different parent directory."

Auto-approve (trusted contributors, i.e. auto_approve_comp_proposals): rename_folder and move_folder get instant approval like upload/replace/move/create_folder. delete and delete_folder are never auto-approved — both always go to manual review regardless of trust status. See Admin: Comp File Proposal Review below for the extra approval gates on delete_folder specifically.

Folder proposal object shape (rename_folder):

{
  "id": 42,
  "contributor_username": "someone",
  "contributor_id": 7,
  "channel_slug": "comp",
  "file_path": "Era Name/Old Folder",
  "destination_path": "Era Name/New Folder",
  "change_type": "rename_folder",
  "staging_filename": "",
  "original_snapshot": {
    "folder": "Era Name/Old Folder",
    "files": [
      "Era Name/Old Folder/song1.mp3",
      "Era Name/Old Folder/song2.mp3"
    ]
  },
  "contributor_notes": "Fixing typo in folder name",
  "status": "pending",
  "reviewer_username": null,
  "review_notes": "",
  "applied_commit_id": "",
  "edit_count": 0,
  "last_edited_at": null,
  "created_at": "...",
  "reviewed_at": null
}

For all three folder change types, staging_filename is always empty (no upload involved), and original_snapshot on move_folder/delete_folder lists every active file the change will affect as { folder, files: [...] }, useful for showing an "affected files" list on a pending proposal.

Comp file proposal object shape:

{
  "id": 55,
  "contributor_username": "someuser",
  "contributor_id": 12,
  "channel_slug": "compilation",
  "file_path": "Compilation/Unreleased/Song.mp3",
  "destination_path": null,
  "change_type": "replace",
  "staging_filename": "staged-a1b2c3.mp3",
  "original_snapshot": { /* file metadata before this change */ },
  "contributor_notes": "Higher quality rip",
  "status": "pending",
  "reviewer_username": null,
  "review_notes": "",
  "applied_commit_id": null,
  "edit_count": 0,
  "created_at": "...",
  "reviewed_at": null
}

status is pending, approved, rejected, or reversed. staging_filename points at the uploaded file sitting in staging until an admin approves it; applied_commit_id is set once approval actually lands the change.

Admin: Comp File Proposal Review

Method Path Description
GET /accounts/admin/comp-proposals/ List all comp-file proposals. Filter: ?status=pending|approved|rejected|reversed&channel=
POST /accounts/admin/comp-proposals/{id}/review/ Approve or reject a proposal
POST /accounts/admin/comp-proposals/{id}/reverse/ Reverse a previously approved proposal
GET /accounts/admin/comp-proposals/{id}/staging/ Download the staged file to inspect before approving
POST /accounts/admin/comp-proposals/{id}/review/

{
  "action": "approve",          // "approve" | "reject"
  "review_notes": "optional",
  "channel": "optional — the proposal's channel slug"
}

/reverse/ and /staging/ also accept ?channel=.

Unlike song-data proposals, there is no "revise" action here — a comp-file change is either accepted as staged or rejected, since there's no meaningful way to hand-edit a binary file upload.

/staging/ streams the actual staged file (not JSON) — treat it as a download/preview link, the same way /files/download/ is used for library audio.

delete_folder — stricter approval rules: on top of the normal review endpoint above, a delete_folder proposal is (1) never auto-approved, always sitting pending until a human reviews it, (2) only approvable by a full is_administrator (or superuser) — a channel manager alone gets a 403, and (3) not self-approvable — the approver must be a different administrator than whoever proposed the deletion, even if the proposer is also an admin.

Status detail
403 Folder deletions require administrator approval.
403 Folder deletions must be approved by a different administrator.

Frontend should hide/disable the approve button for non-administrators on delete_folder proposals, show a notice that it needs a second administrator, and if the current user is the proposer, make clear they can't self-approve even as an admin.

Reversal works the same way as other comp proposals (/reverse/): rename_folder/move_folder move the folder back and restore all file paths and Song.path values; delete_folder restores the archived files to their original paths, recreates the folder, and reactivates their CompFile rows.

Admin: Comp File History

Method Path Description
GET /accounts/admin/comp-files/{filepath}/history/ Every revision applied to a given compilation file path
{
  "filepath": "Compilation/Unreleased/Song.mp3",
  "revisions": [
    {
      "id": 9,
      "filepath": "Compilation/Unreleased/Song.mp3",
      "hash": "d199a85e510b32b9ef3c02a29044a41d",
      "size": 7381244,
      "archive_path": "archive/Compilation/Unreleased/Song.mp3.v9",
      "proposal_id": 55,
      "commit_id": "c_9f2a",
      "is_current": true,
      "created_at": "..."
    }
  ]
}

Exactly one revision per file has is_current: true. Older revisions stay addressable via archive_path for rollback/audit, even after a newer one supersedes them.

Two-Factor (OTP)

Method Path Description
GET /accounts/otp/setup/ Generate a new OTP secret + QR code for enrolling
POST /accounts/otp/setup/ Confirm enrollment with a code from the authenticator app
// GET response
{
  "otp_enabled": false,
  "account_label": "someuser",
  "otp_secret": "JBSWY3DPEHPK3PXP",
  "provisioning_uri": "otpauth://totp/...",
  "qr_code": "data:image/png;base64,..."
}

// POST request
{ "otp_token": "123456" }
// Response: { "otp_enabled": true }

Clone this wiki locally