Repository navigation
Auth and Accounts
| 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
-
GET /accounts/auth/discord/url/→ returnsauthorize_urlandstate - Redirect the user through Discord OAuth using
authorize_url - 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.
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" }POST /accounts/logout/
Authorization: Token <token>
Invalidates the token server-side. Clear the locally stored token regardless of whether this call succeeds.
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
}
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.
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 eachplaycount— 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 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_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.
| 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.
| 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.
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).
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
}
]
}
]| 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).
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).
| 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.
| 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.
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.
| 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.
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.
| 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.
| 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.
| 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 }