API Overview¶
fetchly exposes a JSON HTTP API. It is the same API the web UI uses — there is no separate surface and no second authentication scheme.
Base URL¶
Routes are absolute; there is no shared /api prefix for every family (/download/{id} and /share/{token} sit at the root).
Authentication¶
The API uses the session cookie, exactly like the browser. There is no API token and no bearer scheme. See API Authentication.
When authentication is disabled, every endpoint is open.
OpenAPI¶
The app is built on FastAPI, so the generated schema is available at the framework defaults:
https://fetchly.example.com/docs # Swagger UI
https://fetchly.example.com/redoc # ReDoc
https://fetchly.example.com/openapi.json # Raw schema
These are not public endpoints
They are gated by the same session as everything else once authentication is on. Do not expose them anonymously.
Endpoint families¶
| Family | Purpose |
|---|---|
/health | Unauthenticated health probe |
/login, /logout | Session lifecycle |
/api/submit, /api/jobs/* | Create, list, cancel, retry, and remove jobs |
/api/info | Resolve metadata for a URL before submitting |
/events, /api/jobs/{id}/events | Server-Sent Event streams |
/download/{id}, /audio-source/{id}, /thumbnail/{id} | Media delivery |
/api/thumbnail/*, /api/thumbnail-proxy | Thumbnail resolution and caching |
/api/trim/* | Audio trimming |
/api/lalal/* | Lalal.ai auth and stem separation |
/api/cookies/* | Per-platform cookie import and status |
/api/share/{id}, /share/{token} | Share link creation and redemption |
/api/settings | Read and write runtime settings |
/api/stats, /api/stats/* | Dashboard statistics |
/api/updates, /api/system/host | Version checks and host resources |
The full list is in Endpoints.
Content types¶
| Direction | Type |
|---|---|
| Most requests | application/json |
POST /api/submit | application/x-www-form-urlencoded |
| Cookie file upload | multipart/form-data |
| SSE responses | text/event-stream |
| Media responses | The file's own type |
CSRF¶
/login, /logout, and every state-changing route under /api/* are CSRF-protected with a double-submit cookie (fetchly_csrf) — not just /api/submit. Send the token back either in the X-CSRF-Token header or, for form bodies, as a csrf_token field. A mismatch returns 403. Safe methods (GET, HEAD, OPTIONS) are never checked.
Status codes¶
| Code | Meaning |
|---|---|
200 | Success |
303 | Redirect after a successful form login |
400 | Validation error — the message says what |
401 / 403 | No valid session, or a CSRF failure |
404 | Not found, or deliberately indistinguishable (share links) |
409 | Duplicate job; the existing job is attached |
413 | Upload too large |
429 | Rate limit exceeded |
503 | Job queue full |
Errors are FastAPI's shape:
Rate limits¶
Every route is limited per client IP. See Rate Limiting.
Example session¶
BASE=https://fetchly.example.com
JAR=$(mktemp)
# 1. Fetch the login page: sets the CSRF cookie and embeds the anti-bot token
curl -sc "$JAR" "$BASE/login" -o /tmp/login.html
CSRF=$(awk '$6=="fetchly_csrf"{print $7}' "$JAR")
CAPTCHA=$(grep -oP 'name="captcha_token"[^>]*value="\K[^"]+' /tmp/login.html)
# 2. Log in. POST /login takes JSON; wait ~1 s, because a submission that
# arrives faster than that trips the time trap.
sleep 1
curl -sb "$JAR" -c "$JAR" -X POST "$BASE/login" \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d "{\"username\":\"admin\",\"password\":\"...\",\"captcha_token\":\"$CAPTCHA\",\"honeypot\":\"\"}"
# 3. Use the session
curl -sb "$JAR" "$BASE/api/jobs?limit=10"
# 4. Submit a download (form-encoded, CSRF-protected)
curl -sb "$JAR" -X POST "$BASE/api/submit" \
-H "X-CSRF-Token: $CSRF" \
-d "url=https://www.youtube.com/watch?v=..." \
-d "type=audio" -d "quality=max"
Automating against an instance you control
If the instance is on a private network and only ever driven by scripts, leaving authentication off removes the login dance entirely. Only do that where nothing untrusted can reach the port.
Live updates¶
Prefer SSE over polling:
The stream emits job state changes plus a keep-alive every 5 seconds. Buffering proxies break it — see Reverse Proxy.