API Authentication¶
fetchly has one authentication mechanism: the session cookie. The API and the web UI share it.
No API tokens
There is no bearer token, no API key, and no separate machine scheme. A single-admin application with no per-object ownership has nothing a second credential type would buy.
Two modes¶
| Authentication | Behaviour |
|---|---|
| Disabled (default) | Every endpoint is open; requests run as the internal identity local |
| Enabled | Every endpoint except the public ones requires a valid session cookie |
Public regardless of the setting:
| Route | Why |
|---|---|
GET /health | Orchestrator probes |
GET /login, POST /login | The login itself |
GET /share/{token} | Share links target people without accounts |
GET /static/* | Assets |
Logging in¶
POST /login takes a JSON body and is CSRF-protected.
{
"username": "admin",
"password": "...",
"captcha_token": "<from the login page>",
"honeypot": ""
}
| Field | Required | Notes |
|---|---|---|
username | yes | Letters, hyphens, and underscores only; at most 64 characters |
password | yes | 8–1024 characters |
captcha_token | yes, when authentication is on | Rendered into the login page as a hidden input |
honeypot | yes, and must be empty | The field name comes from the page; leaving it out entirely also passes |
Extra fields are rejected — the model forbids them.
Response:
with Set-Cookie: fetchly_session=....
| Failure | Status | Body |
|---|---|---|
| Anti-bot check failed | 400 | Generic "couldn't verify your submission" |
| Wrong credentials | 401 | Invalid credentials |
| CSRF failure | 403 | detail explains |
| Too many attempts | 429 | Rate limited (5/minute) |
The anti-bot check has a minimum age
A login submitted less than 1 second after the page was served is rejected. A script must fetch /login first, and wait. Tokens stay valid for 6 hours and are not single-use.
CSRF¶
/login, /logout, and the entire /api prefix are protected by a double-submit cookie — not just /api/submit. Every POST/PUT/PATCH/DELETE under /api/* (settings writes, job cancel/retry/remove-all, cookie import/delete, trim create/delete, Lalal.ai actions, share link creation, and more) requires the token. Safe methods (GET, HEAD, OPTIONS) are never checked, protected or not.
GET /loginsets thefetchly_csrfcookie and embeds the same value in the page- Send it back in the
X-CSRF-Tokenheader, or as acsrf_tokenform field - A mismatch returns
403
Using the session¶
The cookie is HttpOnly, SameSite=Lax, and Secure when FETCHLY_BEHIND_HTTPS=1 or the request arrives over HTTPS.
Session lifetime¶
| Limit | Value |
|---|---|
| Hard | 24 hours from login, not configurable |
| Idle | Sliding, session_idle_minutes (1–1440, default 60) |
Both are enforced server-side and reflected in the cookie's Max-Age. A client that sits idle past the timeout gets 401/403 and must log in again.
Logging out¶
A complete script¶
#!/usr/bin/env bash
set -euo pipefail
BASE="https://fetchly.example.com"
USER="admin"
PASS="..."
JAR="$(mktemp)"
PAGE="$(mktemp)"
trap 'rm -f "$JAR" "$PAGE"' EXIT
# Fetch the login page: CSRF cookie + anti-bot token
curl -sc "$JAR" "$BASE/login" -o "$PAGE"
CSRF="$(awk '$6=="fetchly_csrf"{print $7}' "$JAR")"
CAPTCHA="$(grep -oP 'name="captcha_token"[^>]*value="\K[^"]+' "$PAGE")"
# The time trap rejects anything faster than a second
sleep 1.5
curl -sb "$JAR" -c "$JAR" -X POST "$BASE/login" \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d "$(jq -nc --arg u "$USER" --arg p "$PASS" --arg c "$CAPTCHA" \
'{username:$u, password:$p, captcha_token:$c, honeypot:""}')" \
>/dev/null
# Authenticated from here on
curl -sb "$JAR" "$BASE/api/jobs?limit=5" | jq .
Scripting an instance you control
On a private network with nothing untrusted able to reach the port, leaving authentication off removes the login flow entirely. That is a deliberate trade — see Best Practices.
Rate limits on auth routes¶
| Route | Limit |
|---|---|
GET /login | 20/minute |
POST /login | 5/minute |
POST /logout | 20/minute |
Limits are per client IP, so FORWARDED_ALLOW_IPS must name your proxy or every client shares one bucket. See Rate Limiting.