Endpoints¶
Every route, grouped by family. All limits are per client IP.
Generated schema
FastAPI publishes the live schema at /openapi.json, with Swagger UI at /docs. Once authentication is on, those are gated like everything else.
Health¶
GET /health¶
Unauthenticated liveness probe. Used by the container health check.
Pages¶
| Route | Limit | Description |
|---|---|---|
GET / | — | Dashboard |
GET /settings | — | Settings page |
GET /login | 20/min | Login page (sets the CSRF cookie, embeds the anti-bot token) |
GET /job/{job_id} | 120/min | Job detail page: player, waveform, metadata |
GET /favicon.ico | — | Favicon |
Authentication¶
| Route | Limit | Body |
|---|---|---|
POST /login | 5/min | JSON: username, password, captcha_token, honeypot |
POST /logout | 20/min | — |
See API Authentication.
Jobs¶
POST /api/submit¶
Form-encoded, CSRF-protected. 10/minute. Like every other mutating route under /api/*, it requires the X-CSRF-Token header (or a csrf_token field) — see API Authentication.
| Field | Values |
|---|---|
url | A YouTube, TikTok, Instagram, or Facebook URL |
type | audio, video |
quality | max, medium, small |
confirm_duplicate | true to accept a duplicate |
Returns the created job. 409 with {"detail": "duplicate_job", "existing_job": {...}} when an active or completed job already matches. 503 when the queue is full.
The metadata probe runs with an 8 s budget and fills video_title and duration_seconds on the new row, so the job carries a title and a length before the download starts. A probe that fails or times out leaves both null and the job is still created.
GET /api/jobs¶
60/minute. Query: offset (≥ 0, default 0), limit (1–100, default 50).
Returns an array of job objects:
[
{
"id": "0f5c...", "url": "https://...", "platform": "youtube",
"video_title": "...", "video_meta_hover": "...",
"type": "audio", "quality": "max", "status": "analysis_done",
"created_at": "2026-09-01 12:00:00", "finished_at": "2026-09-01 12:01:14",
"message": null, "filesize_bytes": 5242880, "duration_seconds": 214,
"codec": "mp3", "bitrate_kbps": 192,
"bpm": 128, "bpm_confidence": 0.87,
"audio_hash": "...", "filename": "Some Track.mp3"
}
]
GET /api/jobs/{job_id}¶
60/minute. One job, same shape. 404 if unknown.
POST /api/jobs/{job_id}/cancel¶
30/minute. Terminates the running subprocess.
POST /api/jobs/{job_id}/retry¶
10/minute. Allowed only from error and cancelled — retrying an in-flight or successful job would race the worker or duplicate a finished download. The existing row is reset in place, keeping its id and its position in the list, and every result field from the failed attempt (message, filesize, codec, …) is cleared so no stale data shows while the retry runs.
POST /api/jobs/remove-all¶
2/minute. Deletes every job row and its artifacts. No undo.
GET /api/info¶
20/minute. Query: url. Resolves title, duration, and thumbnail before submitting.
Events (SSE)¶
| Route | Limit | Stream |
|---|---|---|
GET /events | 30/min | All job state changes |
GET /api/jobs/{job_id}/events | 30/min | One job |
text/event-stream, keep-alive every 5 s, 32-event queue per client, 200 concurrent connections maximum.
Media¶
| Route | Limit | Description |
|---|---|---|
GET /download/{job_id} | 30/min | The finished file; the name carries the BPM tag when known |
GET /audio-source/{job_id} | 60/min | Audio stream for the player and waveform |
GET /thumbnail/{job_id} | 120/min | The job's thumbnail |
GET /api/thumbnail/resolve | 30/min | Resolve a media URL to a cached thumbnail URL |
GET /api/thumbnail-cache/{cache_key} | 120/min | Serve a cached thumbnail |
GET /api/thumbnail-proxy | 30/min | Proxy a remote thumbnail |
Thumbnail routes validate the content type against the actual bytes, and only fetch from allow-listed hosts.
Trimming¶
| Route | Limit | Description |
|---|---|---|
POST /api/trim/{job_id} | 10/min | Body: start, end (seconds, float) |
DELETE /api/trim/{job_id} | 30/min | Remove the job's trim outputs |
GET /api/trim/{job_id}/{trim_id}/download | 30/min | Download one trim |
Audio jobs only, downloadable status, 1 second to 10 minutes. trim_id is <start_ms>_<end_ms>. See Audio Trimming.
Lalal.ai¶
| Route | Limit | Description |
|---|---|---|
GET /api/lalal/status | 30/min | Auth and validation state plus remaining_minutes (null when unknown); force_refresh=true re-checks |
POST /api/lalal/auth/activation-key | 5/min | Store and validate an activation key |
POST /api/lalal/auth/logout | 10/min | Clear key, email, and cached validation |
POST /api/lalal/{job_id} | 5/min | Query: stem (vocals/instrumental), trimmed, trim_id |
GET /api/lalal/download/{job_id} | 10/min | Query: stem |
Lalal.ai always performs a vocals/instrumental split; stem selects which result comes back. See Stem Separation.
Cookies¶
| Route | Limit | Description |
|---|---|---|
GET /api/cookies | 30/min | Validity snapshot for every platform |
POST /api/cookies/{platform} | 10/min | multipart/form-data upload of a jar |
POST /api/cookies/{platform}/paste | 10/min | JSON {"text": "..."} — cURL, fetch, header, or JSON export |
DELETE /api/cookies/{platform} | 10/min | Remove that platform's jar |
platform is youtube, tiktok, instagram, or facebook. Max 256 KiB, UTF-8. See Platform Cookies.
Share links¶
| Route | Limit | Description |
|---|---|---|
POST /api/share/{job_id} | 20/min | Create or reuse a link |
GET /share/{token} | 20/min | Public. Redeem a link |
Every redeem failure returns the same 404 page. See Share Links.
Settings¶
| Route | Limit | Description |
|---|---|---|
GET /api/settings | 60/min | Current settings; secrets excluded |
POST /api/settings | 5/min | Write an allow-listed subset |
GET /api/settings/watermark-logo | 60/min | Whether a custom watermark logo is installed, and its size |
GET /api/settings/watermark-logo/image | 60/min | The stored logo as PNG (404 when none) |
POST /api/settings/watermark-logo | 10/min | Install a logo (multipart file, PNG, max 2 MB) |
DELETE /api/settings/watermark-logo | 10/min | Remove it; the bundled artwork applies again |
Writable keys: retention_days, enable_authentication, admin_username, admin_password, session_idle_minutes, download_concurrent_fragments, download_compatible_output, video_watermark, lalalaai_duration_guard, share_link_max_uses, public_hostname.
- Unknown keys are silently ignored, not rejected with an error
- Values are parsed and range-checked server-side
download_concurrent_fragmentsaccepts0for automatic host-based sizingadmin_usernameandadmin_passwordmust be sent as a pair — the salt is username-derived, so a rename re-hashes and needs the plaintext- Enabling authentication without stored credentials returns
400 - Internal keys (
admin_password_hash,session_version,statistics_reset_at) are never writable - The watermark logo endpoint takes a PNG, never an SVG: the Settings page accepts both, uploads a PNG unchanged and rasterizes a dropped SVG in the browser first. The server re-derives format, size, aspect ratio and transparency from the uploaded bytes and answers
400with the reason when any of them fails — see Downloads
Statistics and system¶
| Route | Limit | Description |
|---|---|---|
GET /api/stats | 30/min | Aggregate dashboard counters (short-TTL cache) |
POST /api/stats/reset | 5/min | Stamp a reset marker; job history is kept |
GET /api/stats/bpm-clusters | 30/min | limit 1–2000; 5-BPM buckets, count-sorted |
GET /api/updates | 30/min | Version check for fetchly and bundled components (24 h cache) |
GET /api/system/host | 60/min | Host storage, CPU, RAM, uptime |
Error shape¶
| Code | Meaning |
|---|---|
400 | Validation error |
401 | Invalid credentials or no session |
403 | CSRF failure or forbidden |
404 | Not found (or deliberately indistinguishable) |
409 | Duplicate job |
413 | Payload too large |
429 | Rate limited |
503 | Queue full |