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:8080 and MOSAIC_API_KEY=... exported.
  • You’re using -k / verify=False for 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})
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.