Worked Examples
End-to-end auth + add + monitor flows in curl, Python, and JavaScript.
These examples assume:
- The remote interface is enabled and bound to
https://localhost:8080. - You have a rotated API key — copy it from Settings → Web Interface → API key.
- Your shell has
MOSAIC=https://localhost:8080andMOSAIC_API_KEY=...exported. - You’re using
-k/verify=Falsefor the self-signed cert. In production, pin the cert or trust the CA.
curl
1. List torrents
curl -k "$MOSAIC/api/torrents" \
-H "Authorization: Bearer $MOSAIC_API_KEY" | jq .
2. Add a magnet, set its category, watch it complete
INFOHASH=$(curl -k -s -X POST "$MOSAIC/api/torrents/magnet" \
-H "Authorization: Bearer $MOSAIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"magnet":"magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c&dn=Big+Buck+Bunny","save_path":""}' \
| jq -r .id)
echo "Added $INFOHASH"
# Set category
curl -k -X POST "$MOSAIC/api/torrents/category" \
-H "Authorization: Bearer $MOSAIC_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"infohash\":\"$INFOHASH\",\"category_id\":1}"
# Poll until done
until [ "$(curl -k -s "$MOSAIC/api/torrents" -H "Authorization: Bearer $MOSAIC_API_KEY" \
| jq -r ".[] | select(.id==\"$INFOHASH\") | .completed")" = "true" ]; do
sleep 5
done
echo "Done."
3. Upload a .torrent file with multipart
curl -k -X POST "$MOSAIC/api/torrents/file" \
-H "Authorization: Bearer $MOSAIC_API_KEY" \
-F "file=@./ubuntu-24.04-desktop-amd64.iso.torrent" \
-F "save_path=/srv/downloads/iso"
4. Trigger an update check + install
INFO=$(curl -k -s -X POST "$MOSAIC/api/updater/check" \
-H "Authorization: Bearer $MOSAIC_API_KEY")
echo "$INFO" | jq .
if [ "$(echo "$INFO" | jq -r .available)" = "true" ]; then
curl -k -X POST "$MOSAIC/api/updater/install" \
-H "Authorization: Bearer $MOSAIC_API_KEY"
fi
Python
# pip install requests websocket-client
import os, json, ssl, requests, websocket
BASE = os.environ["MOSAIC"] # e.g. "https://localhost:8080"
KEY = os.environ["MOSAIC_API_KEY"]
HEAD = {"Authorization": f"Bearer {KEY}"}
# requests session with self-signed cert tolerated; pin in production
S = requests.Session()
S.verify = False
import urllib3; urllib3.disable_warnings()
1. Add a magnet and set per-file priorities once metadata resolves
def add_magnet(uri, save_path=""):
r = S.post(f"{BASE}/api/torrents/magnet",
json={"magnet": uri, "save_path": save_path}, headers=HEAD)
r.raise_for_status()
return r.json()["id"]
def list_torrents():
r = S.get(f"{BASE}/api/torrents", headers=HEAD)
r.raise_for_status()
return r.json()
def focus(id, tabs):
S.post(f"{BASE}/api/inspector/focus", json={"id": id, "tabs": tabs}, headers=HEAD).raise_for_status()
def set_priorities(infohash, prios):
# prios: dict[int, "skip"|"normal"|"high"|"max"]
body = {"infohash": infohash, "priorities": {str(k): v for k, v in prios.items()}}
S.post(f"{BASE}/api/torrents/file_priorities", json=body, headers=HEAD).raise_for_status()
2. Stream the WebSocket and react to ticks
def on_message(ws, raw):
env = json.loads(raw)
t = env["type"]
if t == "torrents:tick":
print(f"[tick] {len(env['payload'])} torrents")
elif t == "stats:tick":
s = env["payload"]
print(f"[stats] dl {s['total_download_rate']/1024:.1f} KB/s ul {s['total_upload_rate']/1024:.1f} KB/s")
elif t == "inspector:tick":
d = env["payload"]
print(f"[inspect] {d['name']}: {d['progress']*100:.1f}%")
elif t == "update:available":
print(f"[update] {env['payload']['current_version']} → {env['payload']['latest_version']}")
host = BASE.split("://", 1)[1]
ws_url = f"wss://{host}/api/ws?key={KEY}"
ws = websocket.WebSocketApp(ws_url, on_message=on_message)
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
3. Cookie-based session login (alternative to bearer)
r = S.post(f"{BASE}/api/login", json={"username": "admin", "password": "hunter2"})
r.raise_for_status()
# requests stores the mosaic_session cookie on S.cookies; subsequent calls work without HEAD
S.get(f"{BASE}/api/torrents")
For state-changing requests with cookie auth, you must include an Origin header that matches the host:
S.post(f"{BASE}/api/torrents/magnet",
json={"magnet": uri, "save_path": ""},
headers={"Origin": BASE})
…or just use the bearer header to skip the CSRF guard entirely.
JavaScript (Node + browser)
Browser SPA
The Mosaic frontend itself is a worked example — see frontend/src/lib/transport.ts and frontend/src/lib/bindings.ts in the repo. The pattern, condensed:
const KEY = '...';
const wsURL = `wss://${location.host}/api/ws?key=${encodeURIComponent(KEY)}`;
const HEAD = { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' };
async function listTorrents() {
const r = await fetch('/api/torrents', { headers: HEAD });
if (!r.ok) throw new Error(await r.text());
return r.json();
}
async function addMagnet(magnet, save_path = '') {
const r = await fetch('/api/torrents/magnet', {
method: 'POST', headers: HEAD,
body: JSON.stringify({ magnet, save_path }),
});
return (await r.json()).id;
}
function connect() {
const ws = new WebSocket(wsURL);
ws.onmessage = ({ data }) => {
const { type, payload } = JSON.parse(data);
if (type === 'torrents:tick') store.setTorrents(payload);
else if (type === 'stats:tick') store.setStats(payload);
else if (type === 'inspector:tick') store.setDetail(payload);
else if (type === 'update:available') showUpdateToast(payload);
};
ws.onclose = () => setTimeout(connect, 2000);
}
Node script
// npm i node-fetch ws
import fetch from 'node-fetch';
import WebSocket from 'ws';
import https from 'https';
const BASE = 'https://localhost:8080';
const KEY = process.env.MOSAIC_API_KEY;
const agent = new https.Agent({ rejectUnauthorized: false }); // self-signed
const HEAD = { 'Authorization': `Bearer ${KEY}` };
const torrents = await fetch(`${BASE}/api/torrents`, { headers: HEAD, agent }).then(r => r.json());
console.log(`${torrents.length} torrents`);
// Tail the WS
const ws = new WebSocket(`wss://localhost:8080/api/ws?key=${encodeURIComponent(KEY)}`, { rejectUnauthorized: false });
ws.on('message', (raw) => {
const env = JSON.parse(raw.toString());
if (env.type === 'stats:tick') {
const s = env.payload;
process.stdout.write(`\rdl ${(s.total_download_rate/1024).toFixed(1)} KB/s ul ${(s.total_upload_rate/1024).toFixed(1)} KB/s `);
}
});
Common errors
| Symptom | Likely cause |
|---|---|
401 {"error":"unauthorized"} |
Missing/invalid bearer key, or cookie expired (12h TTL). |
403 {"error":"origin mismatch"} |
Cookie-authed POST/PUT/DELETE without a matching Origin header. |
429 {"error":"too many login attempts"} |
Login rate limiter tripped — wait Retry-After seconds. |
| TLS warnings in scripts | Self-signed cert; pin or trust per browser/system. |
| WS reconnect loop | API key was rotated mid-session, or the session cookie expired. |
400 from POST /torrents/magnet |
Magnet failed engine validation. Verify the URI is well-formed. |
502 from POST /settings/blocklist/refresh |
Blocklist URL didn’t return — check the URL or DNS. |