-
-
Notifications
You must be signed in to change notification settings - Fork 81
Web API
Last validated against qbit_manage v4.7.1. See v4 Migration Guide for changes since v4.0.
qBit Manage provides a REST API that allows you to trigger commands via HTTP requests. The API server runs at 8181, listening to all hostnames by default, and can be configured using the --host and --port options or QBT_HOST and QBT_PORT environment variables.
python qbit_manage.py --web-server --host 0.0.0.0 --port 8181services:
qbit_manage:
image: ghcr.io/stuffanthings/qbit_manage:latest
container_name: qbit_manage
environment:
- QBT_WEB_SERVER=true # Enable web server (set to false to disable)
- QBT_HOST=0.0.0.0 # Set web server host
- QBT_PORT=8181 # Set web server port
ports:
- "8181:8181" # Map container port to host
volumes:
- /path/to/config:/configMost endpoints require authentication via API key or basic auth when authentication is enabled. Exceptions:
GET /api/health,GET /api/version, andGET /api/get_base_urlare always public (unconditionally bypassed byAuthenticationMiddlewarepermodules/auth.pyskip_auth_paths), even when authentication is enabled.
| Endpoint | Method | Auth required |
|---|---|---|
/api/run-command |
POST | Yes |
/api/configs |
GET | Yes |
/api/configs/{filename} |
GET | Yes |
/api/configs/{filename} |
POST | Yes |
/api/configs/{filename} |
PUT | Yes |
/api/configs/{filename} |
DELETE | Yes |
/api/configs/{filename}/validate |
POST | Yes |
/api/configs/{filename}/backup |
POST | Yes |
/api/configs/{filename}/backups |
GET | Yes |
/api/configs/{filename}/restore |
POST | Yes |
/api/scheduler |
GET | Yes |
/api/schedule |
PUT | Yes |
/api/schedule/persistence/toggle |
POST | Yes |
/api/logs |
GET | Yes |
/api/log_files |
GET | Yes |
/api/docs |
GET | Yes |
/api/version |
GET | No (always public) |
/api/health |
GET | No (always public) |
/api/get_base_url |
GET | No (always public) |
/api/security |
GET | Yes |
/api/security/status |
GET | Yes |
/api/security |
PUT | Yes |
/api/system/force-reset |
POST | Yes |
List all available config files in the config directory. Sensitive files (qbm_settings.yml, secrets.yml, etc.) are automatically filtered from results.
Response:
{
"configs": ["config.yml", "config2.yml"],
"default_config": "config.yml"
}Fetch the contents of a specific config file as a parsed object.
Response:
{
"filename": "config.yml",
"data": { "qbt": { "host": "localhost:8080" }, "settings": {} },
"last_modified": "2026-05-21T10:00:00",
"size": 4096
}data mirrors the YAML structure. !ENV variable references are preserved as strings in the form !ENV <VAR>.
Create a new config file. Returns 409 if the file already exists.
Request body:
{
"data": {
"qbt": { "host": "localhost:8080", "user": "admin", "pass": "" },
"settings": { "run_now": false }
}
}Response (201-equivalent):
{ "status": "success", "message": "Configuration 'config2.yml' created successfully" }Update an existing config file. A timestamped backup is created automatically before writing. Returns 404 if file does not exist.
Request body (same shape as POST — full config data object):
{
"data": {
"qbt": { "host": "localhost:8080", "user": "admin", "pass": "" },
"settings": { "run_now": false, "schedule": 1440 },
"cat": { "movies": "/data/torrents/movies" }
}
}Response:
{ "status": "success", "message": "Configuration 'config.yml' updated successfully" }Delete a config file permanently. A backup is created before deletion.
Response:
{ "status": "success", "message": "Configuration 'config.yml' deleted successfully" }Validate a config file for correctness by running it through qbit_manage's config parser without executing commands. If validation causes defaults to be written (e.g., missing keys are backfilled), those changes are applied to the actual config file.
Request body:
{
"data": {
"qbt": { "host": "localhost:8080" },
"settings": {},
"cat": {},
"tracker": {},
"share_limits": {}
}
}Response (valid):
{
"valid": true,
"errors": [],
"warnings": [],
"config_modified": false
}Response (invalid — with errors):
{
"valid": false,
"errors": ["Config Error: qbt.host is required"],
"warnings": [],
"config_modified": false
}config_modified: true means defaults were written back to the on-disk config during validation.
Create a timestamped backup of the specified config file. Backups are retained up to 30 per config.
Response:
{
"status": "success",
"message": "Manual backup created successfully",
"backup_file": "config_20260521_100000.yml"
}List all available backups for the specified config file.
Response:
{
"backups": [
{ "filename": "config_20260521_100000.yml", "created": "2026-05-21T10:00:00", "size": 4096 }
]
}Restore a config file from a previously created backup. Pass the backup filename as the request body.
Return the current scheduler status, including schedule expression and next run time.
Response:
{
"current_schedule": { "type": "interval", "value": "1440" },
"next_run": "2026-05-22T10:00:00.000000",
"next_run_str": "in 24 hours",
"is_running": false,
"source": "env",
"persistent": false,
"file_exists": false,
"disabled": false
}type is "interval" (minutes) or "cron" (cron expression). source is "env" (from QBT_SCHEDULE), "file" (persisted via API), or null.
Set or update the scheduler's cron/interval expression. The change takes effect immediately and can optionally be persisted across restarts.
Request body:
{ "schedule": "1440", "type": "interval" }Or using a cron expression:
{ "schedule": "0 4 * * *", "type": "cron" }type is optional — qbit_manage will auto-detect "interval" vs "cron" if omitted. Interval values must be positive integers (minutes).
Response:
{
"success": true,
"message": "Schedule saved successfully: interval=1440",
"schedule": "1440",
"type": "interval",
"persistent": true
}Toggle whether the scheduler's configuration persists across restarts.
Fetch recent log content from the log files directory.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | none (all) | Maximum number of lines to return (most recent N lines) |
log_filename |
str | qbit_manage.log |
Name of the log file to read |
Response:
{
"logs": [
"2026-05-21 10:00:00,000 INFO | Run started",
"2026-05-21 10:00:01,123 INFO | Processing torrent: Example.Torrent"
]
}Lines are returned in chronological order (oldest first). Use limit to cap output for large log files.
log_filename accepts the active log (<stem>.log, e.g. qbit_manage.log) or a rotated log (<stem>.<N>.log, e.g. qbit_manage.1.log). Any other name returns 400.
Example:
curl -s "http://localhost:8181/api/logs?limit=50&log_filename=qbit_manage.log" \
-H "X-API-Key: your_api_key_here"List all available log files in the logs directory.
Response:
{ "log_files": ["qbit_manage.log", "qbit_manage.1.log"] }Return documentation metadata for the API (markdown file content).
Return version information for qBit Manage. Always public — no auth required.
Response:
{
"version": "4.7.1",
"branch": "master",
"build": 0,
"latest_version": "4.7.1",
"update_available": false
}update_available: true when a newer release is available on GitHub. build is the git commit count (integer).
Liveness and readiness probe. Always public — no auth required. Returns full application state.
Response:
{
"status": "healthy",
"timestamp": "2026-05-21T10:00:00.000000",
"version": "4.7.1",
"branch": "master",
"application": {
"web_api_responsive": true,
"can_accept_requests": true,
"queue_size": 0,
"has_queued_requests": false,
"next_scheduled_run": "2026-05-22T10:00:00.000000",
"next_scheduled_run_text": "in 24 hours"
},
"directories": {
"config_dir_exists": true,
"config_files_count": 1,
"logs_dir_exists": true,
"recent_log_entries": 150,
"last_activity": "Recent activity detected"
},
"issues": []
}status values: "healthy" | "degraded" (config/log dir missing) | "busy" (run in progress) | "unhealthy" (health check itself failed).
Return the resolved base URL the server is listening on. Always public — no auth required.
Response:
{ "baseUrl": "" }Returns empty string when no --base-url / QBT_BASE_URL is configured.
Return the current security configuration (API key and basic auth settings).
Return a summary of whether authentication is enabled and which methods are active.
Update security settings (API key, basic auth credentials, etc.).
Force-reset the internal running state. Use when a stuck run has left the system in an inconsistent state.
Execute qBit Manage commands via the API.
{
"config_file": "config.yml", // Optional, defaults to "config.yml"
"commands": ["cat_update", "tag_update"], // Required, list of commands to run
"hashes": ["<hash1>", "<hash2>"], // Optional, list of torrent hashes to filter by
"dry_run": false, // Optional, defaults to false
"skip_cleanup": false, // Optional, defaults to false
"skip_qb_version_check": false, // Optional, defaults to false
"log_level": null // Optional, defaults to null (e.g., "info", "debug", "error")
}Success:
{
"status": "success",
"message": "Commands executed successfully for all configs",
"results": [
{
"config_file": "config.yml",
"stats": {
"executed_commands": ["cat_update", "tag_update"],
"categorized": 5,
"tagged": 10
}
}
]
}Queued (when scheduled run is in progress):
{
"status": "queued",
"message": "Scheduled run in progress. Request queued.",
"config_file": "config.yml",
"commands": ["cat_update", "tag_update"]
}Error:
{
"detail": "Error message"
}The following commands can be included in the commands array of the API request:
| Command | Description | Supports Hashes |
|---|---|---|
cat_update |
Update categories based on save paths | Yes |
tag_update |
Add tags based on tracker URLs | Yes |
recheck |
Recheck paused torrents sorted by size and resume completed ones | Yes |
rem_unregistered |
Remove unregistered torrents | Yes |
tag_tracker_error |
Tag torrents with tracker errors | Yes |
rem_orphaned |
Remove orphaned files from root directory | No |
tag_nohardlinks |
Tag torrents with no hard links | Yes |
share_limits |
Apply share limits based on tags/categories | Yes |
Example using multiple commands:
{
"config_file": "config.yml",
"commands": ["cat_update", "tag_update", "share_limits"],
"dry_run": true
}qBittorrent can call a program after a torrent is added or finished. You can configure this in Settings > Downloads > Run external program.
Example command to run after torrent added/finished:
#!/bin/bash
# run_qbit_manage_commands.sh
#
# Sends a POST request to qBit Manage with a given torrent hash to trigger
# actions like "tag_update" and "share_limits".
#
# USAGE:
# ./run_qbit_manage_commands.sh <torrent_hash>
#
# EXAMPLE:
# ./run_qbit_manage_commands.sh 123ABC456DEF789XYZ
#
# NOTES:
# - Make sure this script is executable: chmod +x run_qbit_manage_commands.sh
# - The torrent hash is typically passed in automatically by qBittorrent via the "%I" variable.
# - All output is logged to run_qbit_manage_commands.log in the same directory as the script,
# and also printed to stdout.
set -euo pipefail
API_URL="http://qbit_manage:8181/api/run-command"
COMMANDS='["tag_update", "share_limits"]'
if [[ $# -lt 1 || -z "$1" ]]; then
echo "Usage: $0 <torrent_hash>" >&2
exit 1
fi
TORRENT_HASH="$1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="${SCRIPT_DIR}/run_qbit_manage_commands.log"
JSON="{\"commands\":${COMMANDS},\"hashes\":[\"${TORRENT_HASH}\"]}"
{
echo "Sending API call for hash: ${TORRENT_HASH}"
echo "Payload: ${JSON}"
} | tee -a "${LOG_FILE}"
if curl -fsSL -X POST \
-H "Content-Type: application/json" \
-d "${JSON}" \
"${API_URL}" | tee -a "${LOG_FILE}"; then
echo "Success" | tee -a "${LOG_FILE}"
else
echo "Error: qBit Manage API call failed for hash ${TORRENT_HASH}" | tee -a "${LOG_FILE}"
fiTo use this script:
- Save it as a file (e.g.,
run_qbit_manage_commands.sh). - Make the script executable:
chmod +X /path/to/run_qbit_manage_commands.sh
- In qBittorrent, configure
Settings > Downloads > Run external programto execute this script, passing%Ias an argument:The script will create a log file named/path/to/run_qbit_manage_commands.sh "%I"run_qbit_manage_commands.login the same directory where the script is located.
-
Concurrent Execution:
- API requests during scheduled runs are automatically queued
- Queued requests are processed after the scheduled run completes
- Double-check mechanism prevents race conditions
-
Command Execution:
- Commands sent to the API are mandatory and the commands defined in ENV variables and in the config file are not used when using the API.
- All regular command validations apply
- Dry run mode available for testing
-
Error Handling:
- Failed requests return appropriate error messages
- Queue state is preserved even during errors
- Proper cleanup ensures system stability
Using curl:
curl -X POST http://localhost:8181/api/run-command \
-H "Content-Type: application/json" \
-d '{
"config_file": "config.yml",
"commands": ["cat_update", "tag_update"],
"hashes": ["a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"],
"dry_run": false,
"skip_cleanup": false,
"skip_qb_version_check": false,
"log_level": "info"
}'If authentication is enabled, include your API key:
curl -X POST http://localhost:8181/api/run-command \
-H "Content-Type: application/json" \
-H "X-API-Key: your_api_key_here" \
-d '{
"config_file": "config.yml",
"commands": ["cat_update", "tag_update"],
"hashes": ["a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"],
"dry_run": false,
"skip_cleanup": false,
"skip_qb_version_check": false,
"log_level": "info"
}'Alternatively, if Basic Authentication is enabled and you don't have an API key:
curl -X POST http://localhost:8181/api/run-command \
-u "username:password" \
-H "Content-Type: application/json" \
-d '{
"config_file": "config.yml",
"commands": ["cat_update", "tag_update"],
"hashes": ["a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"],
"dry_run": false,
"skip_cleanup": false,
"skip_qb_version_check": false,
"log_level": "info"
}'Using Python requests:
import requests
response = requests.post(
"http://localhost:8181/api/run-command",
json={
"config_file": "config.yml",
"commands": ["cat_update", "tag_update"],
"hashes": ["a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"],
"dry_run": False,
"skip_cleanup": False,
"skip_qb_version_check": False,
"log_level": "info"
}
)
print(response.json())If authentication is enabled, include your API key:
import requests
headers = {"X-API-Key": "your_api_key_here"}
response = requests.post(
"http://localhost:8181/api/run-command",
headers=headers,
json={
"config_file": "config.yml",
"commands": ["cat_update", "tag_update"],
"hashes": ["a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"],
"dry_run": False,
"skip_cleanup": False,
"skip_qb_version_check": False,
"log_level": "info"
}
)
print(response.json())Alternatively, if Basic Authentication is enabled:
import requests
from requests.auth import HTTPBasicAuth
response = requests.post(
"http://localhost:8181/api/run-command",
auth=HTTPBasicAuth("username", "password"),
json={
"config_file": "config.yml",
"commands": ["cat_update", "tag_update"],
"hashes": ["a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"],
"dry_run": False,
"skip_cleanup": False,
"skip_qb_version_check": False,
"log_level": "info"
}
)
print(response.json())- All commands must be valid command names from the list above
- Commands are case-sensitive
- Invalid commands will return a 400 error with detail about the invalid command
- Empty command list will return a 400 error
- Commands defined in ENV variables or config files are ignored when using the API
- The API validates commands before execution and during runtime
Example error response for invalid command:
{
"detail": "Invalid command: invalid_command"
}