HTTP API

HTTP API #

SaveAny-Bot provides an HTTP API that allows you to programmatically create download/transfer tasks, query task status, cancel tasks, and more — without going through Telegram.

Enabling the API #

The API shares the web console listener and port; there is nothing separate to enable. The web console listens on 127.0.0.1:28080 by default. Set an access token under Settings → Web:

  • Access token: used for direct Web UI access through /?token=... and with Authorization: Bearer <token> for the API.
  • Web listen address: shared by the console and API; use 0.0.0.0:28080 when it must be reachable from other hosts.

All endpoints live under /api/v1.

Authentication #

Set an access token under Settings → Web, then send it in the request header:

Authorization: Bearer <your-token>

If no access token is configured, a random token is generated on every start and printed in the log as part of the direct Web UI URL. That value also works as a Bearer token for the current process.

You can also use the web console login cookie. Non-GET requests made with a cookie also require the X-CSRF-Token header.

Authentication failures return 401:

{ "error": "unauthorized", "message": "authentication required" }

Error Response Format #

All errors use a consistent JSON format:

{
  "error":   "error_code",
  "message": "human readable description"
}

Common error codes:

Error CodeHTTP StatusMeaning
unauthorized401Authentication failed
invalid_request400Malformed request body or parameters
task_creation_failed400Failed to create task
task_not_found404Task ID does not exist
cancel_failed500Failed to cancel task
internal_error500Internal server error

Endpoints #

GET /health — Health Check #

No authentication required.

Response 200 OK:

{ "status": "ok", "version": "1.0.0" }

GET /api/v1/storages — List Storages #

Returns all currently loaded storage backends.

Response 200 OK:

{
  "storages": [
    {
      "storage_id": 1,
      "name": "local",
      "type": "local",
      "enable": true,
      "config": { "base_path": "/app/downloads" },
      "config_version": 1
    }
  ]
}

GET /api/v1/meta — API and Capability Metadata #

Returns the frozen v1 capability vocabulary and the database schema version.

{
  "api_version": "v1",
  "version": "1.0.0",
  "schema_version": 1,
  "chat_id_format": "bot_api",
  "task_types": ["tgfiles", "tphpics", "parseditem", "directlinks", "aria2", "ytdlp", "transfer"],
  "task_statuses": ["queued", "running", "completed", "partial", "failed", "cancelled"],
  "task_item_statuses": ["completed", "failed", "skipped", "cancelled"],
  "storage_types": ["local", "webdav", "alist", "telegram", "s3", "rclone"],
  "rule_types": ["FILENAME-REGEX", "MESSAGE-REGEX", "IS-ALBUM"],
  "filename_strategies": ["default", "message", "template"],
  "conflict_strategies": ["rename", "ask", "overwrite", "skip"]
}

Chat IDs use the Bot API form: positive IDs for users, negative IDs for basic groups, and -100... IDs for supergroups and channels.


GET /api/v1/task-types — List Supported Task Types #

Response 200 OK:

{
  "types": [
    { "type": "directlinks", "label": "directlinks" },
    { "type": "tgfiles", "label": "tgfiles" }
  ]
}

GET /api/v1/media-metadata — Media Metadata Lookup #

Inspect a Telegram message link, direct media URL, or other yt-dlp-supported media URL and return available metadata without downloading the full file.

Request:

curl -X GET "http://127.0.0.1:28080/api/v1/media-metadata?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dxxx" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response:

{
  "url": "https://www.youtube.com/watch?v=xxx",
  "title": "Example title",
  "thumbnail": "https://example.com/thumb.jpg",
  "uploader": "Example uploader",
  "duration_seconds": 213.0
}

Returns 400 invalid_request when url is missing, or 400 metadata_extraction_failed when Telegram, direct probing, and yt-dlp all fail to inspect the media URL.


POST /api/v1/tasks — Create Task #

Request headers:

Content-Type: application/json
Authorization: Bearer <token>

Request body:

{
  "type":    "<task_type>",
  "storage_id": 1,
  "path":    "<subpath>",
  "webhook": "<callback_url>",
  "params":  { }
}
FieldTypeRequiredDescription
typestringYesTask type — see below
storage_iduintYesTarget storage ID
pathstringNoSubdirectory path within the storage
webhookstringNoCallback URL invoked when the task reaches a terminal state
paramsobjectYesType-specific parameters — see below

Response 201 Created:

{
  "task_id":      "abc123xyz",
  "type":         "directlinks",
  "status":       "queued",
  "storage_id":   1,
  "storage_name": "local",
  "created_at": "2026-03-11T10:00:00Z"
}

Task Types and params #

Download one or more files from direct HTTP/HTTPS URLs.

{
  "type":    "directlinks",
  "storage_id": 1,
  "path":    "downloads",
  "params": {
    "urls": [
      "https://example.com/file.zip",
      "https://example.com/other.zip"
    ]
  }
}
params fieldTypeRequiredDescription
urls[]stringYesList of download URLs, at least 1
ytdlp — yt-dlp Media Download #
Requires yt-dlp to be installed on the system.

Download videos or audio via yt-dlp, supporting YouTube, Bilibili, and 1000+ other sites.

{
  "type":    "ytdlp",
  "storage_id": 1,
  "path":    "videos",
  "params": {
    "urls":  ["https://www.youtube.com/watch?v=xxx"],
    "flags": ["--extract-audio", "--audio-format", "mp3"]
  }
}
params fieldTypeRequiredDescription
urls[]stringYesList of media URLs, at least 1
flags[]stringNoExtra yt-dlp command-line flags; -o/--output only contributes its file name template (the download directory is managed by the bot)
aria2 — Aria2 Download #
Requires Aria2 to be enabled and configured (RPC) under **Settings → Aria2**.

Download files via the Aria2 download manager, supporting HTTP/HTTPS, FTP, BitTorrent (magnet links, torrent files), and more.

{
  "type":    "aria2",
  "storage_id": 1,
  "path":    "downloads",
  "params": {
    "urls":    ["magnet:?xt=urn:btih:..."],
    "options": { "split": "4" }
  }
}
params fieldTypeRequiredDescription
urls[]stringYesList of download URIs, at least 1
optionsmap[string]stringNoAria2 download options
parseditem — Parser Plugin Download #

Hand a URL off to a registered JS plugin or built-in parser for processing and downloading.

{
  "type":    "parseditem",
  "storage_id": 1,
  "path":    "parsed",
  "params": {
    "url": "https://some-site.com/page"
  }
}
params fieldTypeRequiredDescription
urlstringYesThe URL to parse

Returns 400 task_creation_failed if no parser is able to handle the URL.

tgfiles — Telegram Message File Download #

Download files from Telegram messages via message links. Supported link formats:

  • https://t.me/username/123 — public channel or group
  • https://t.me/c/123456789/123 — private channel by numeric ID
  • https://t.me/c/123456789/111/456 — topic message (thread ID / message ID)
  • https://t.me/username/111/456 — topic under a username-based chat

If the message is part of a media group (album), all files in the group are downloaded by default. Append ?single to the link to force downloading only the single specified message.

{
  "type":    "tgfiles",
  "storage_id": 1,
  "path":    "telegram",
  "params": {
    "message_links": [
      "https://t.me/username/123",
      "https://t.me/c/1234567890/456"
    ]
  }
}
params fieldTypeRequiredDescription
message_links[]stringNoList of Telegram message links; required unless chat_id and message_range are used
chat_idstringNoBot API chat ID or username used with message_range
message_rangestringNoInclusive message range, for example 100-120
filterstringNoOptional regular expression matching message text or file names
tphpics — Telegraph Article Images #

Download all images from a Telegra.ph article.

Supported URL prefixes: https://telegra.ph/, http://telegra.ph/, https://telegraph.co/, http://telegraph.co/

{
  "type":    "tphpics",
  "storage_id": 1,
  "path":    "telegraph",
  "params": {
    "telegraph_url": "https://telegra.ph/Some-Article-01-01"
  }
}
params fieldTypeRequiredDescription
telegraph_urlstringYesURL of the Telegra.ph article
transfer — Storage-to-Storage Transfer #

Transfer files directly between two storage backends without going through Telegram. The source storage must support both listing and reading.

For `transfer` tasks, the top-level `storage_id` field is still required for validation, but the actual storages used are determined by `source_storage_id` and `target_storage_id` inside `params`.
{
  "type":    "transfer",
  "storage_id": 1,
  "params": {
    "source_storage_id": 2,
    "source_path":       "backups/",
    "target_storage_id": 1,
    "target_path":       "restored/"
  }
}
params fieldTypeRequiredDescription
source_storage_iduintYesSource storage ID
source_pathstringYesPath within the source storage; must contain at least one file
target_storage_iduintYesTarget storage ID
target_pathstringYesDestination path within the target storage

GET /api/v1/tasks — List All Tasks #

Returns tasks newest first (by creation time). Task records and their creation parameters are persisted in SQLite; queued and interrupted tasks are resumed after a restart. Terminal records remain until deleted through the API.

Response 200 OK:

{
  "tasks": [
    {
      "task_id":    "abc123xyz",
      "type":       "directlinks",
      "status":     "running",
      "title":      "file.zip",
      "storage_id":   1,
      "storage_name": "local",
      "path":       "downloads",
      "error":      "",
      "created_at": "2026-03-11T10:00:00Z",
      "updated_at": "2026-03-11T10:00:05Z",
      "progress": {
        "total_bytes":      10485760,
        "downloaded_bytes": 5242880,
        "total_files":      3,
        "downloaded_files": 1,
        "percent":          50.0,
        "speed_mbps":       12.5
      }
    }
  ],
  "total": 1
}

The progress field is included when either total_bytes or total_files is greater than 0; individual fields within it are omitted when zero. percent is computed from bytes when the byte total is known, and falls back to the file count otherwise. speed_mbps is the average since the task started running, not an instantaneous rate. The error field is only included when non-empty.


GET /api/v1/tasks/{task_id} — Get Task #

Path parameter: task_id — the ID returned when the task was created.

Response 200 OK: Same structure as a single task object from the list above.

Error responses:

  • 400 invalid_request — no task ID in path
  • 404 task_not_found — task does not exist

Task objects include metadata_available when at least one item has stored message metadata.


GET /api/v1/tasks/{task_id}/metadata — Get Message Metadata #

Returns the normalized Telegram message metadata recorded for every saved file in a Telegram file task. Enable Settings → Message metadata before creating the task. Metadata is always stored in the local database. The optional Write sidecar JSON setting also writes <actual-file-path>.meta.json beside the saved file; Telegram storage skips sidecars.

Path parameter: task_id — the task whose metadata should be returned.

Response 200 OK:

{
  "task_id": "abc123xyz",
  "items": [
    {
      "index": 1,
      "name": "saved-video.mp4",
      "path": "downloads/saved-video.mp4",
      "metadata": {
        "version": 1,
        "message": {
          "id": 123,
          "chat": { "id": -1001234567890, "title": "Channel" },
          "sender": { "id": 123456789, "name": "Alice" },
          "text": "caption",
          "grouped_id": 999
        },
        "file": {
          "name": "saved-video.mp4",
          "media_type": "document",
          "mime_type": "video/mp4",
          "size": 12345678,
          "width": 1920,
          "height": 1080,
          "duration": 123.4
        }
      }
    }
  ]
}

Message text is omitted when Store message text is disabled. Access hashes, tokens, and raw Telegram objects are never stored.

Error responses:

  • 400 invalid_request — no task ID in path
  • 404 task_not_found — task does not exist
  • 404 metadata_not_found — the task has no recorded metadata

POST /api/v1/tasks/{task_id}/retry — Retry Failed Files #

Creates a new task from the failed files of a terminal task. The bot fetches the original messages again, which also refreshes their file references. Supported for Telegram file tasks (tgfiles).

Path parameter: task_id — the task to retry.

Response 202 Accepted:

{ "task_id": "d4f7u1crvimcs4n4j8e1", "status": "queued", "file_count": 2 }

Error responses:

  • 409 task_active — the task is still queued or running
  • 409 nothing_to_retry — the task has no failed or unfinished files
  • 409 storage_missing / 409 source_unavailable
  • 400 retry_unsupported — the task type cannot be retried
  • 503 client_unavailable — no Telegram client is running

Task responses include per-file outcomes: list responses carry the total in failed_count plus up to five failed items per task in failed_items, and the single-task response carries every recorded file in items.


POST /api/v1/tasks/{task_id}/cancel — Cancel Task #

Cancels a queued or running task.

Path parameter: task_id — the task to cancel.

Response 202 Accepted:

{ "task_id": "abc123xyz", "status": "cancelled" }

Error responses:

  • 409 task_not_active — the task is already terminal
  • 404 task_not_found — task does not exist
  • 500 cancel_failed — cancellation failed

DELETE /api/v1/tasks/{task_id} — Delete Task #

Deletes a terminal task and its recorded items. Use POST /api/v1/tasks/{task_id}/cancel to cancel a queued or running task.

Path parameter: task_id

Response 204 No Content:

  • 409 task_active — cancel the task before deleting it
  • 404 task_not_found — task does not exist

Task Statuses #

StatusMeaning
queuedTask is queued and waiting to run
runningTask is currently executing
completedTask finished successfully
partialTask finished, but one or more files failed
failedTask encountered an error
cancelledTask was cancelled through the cancel endpoint

Queued tasks and tasks interrupted while running keep their creation parameters and are re-enqueued after a restart. Telegram downloads resume from the partial cache; files whose upload already completed are skipped.


Webhook Callbacks #

When a webhook URL is provided in the create request, SaveAny-Bot sends a POST request to that URL once the task reaches completed, partial, or failed.

Cancelled tasks do **not** trigger a webhook — cancellation is already known to whoever requested it. Each task fires at most one callback.

The callback is sent asynchronously, so the task is not delayed by a slow or unreachable endpoint.

Callback request headers:

Content-Type: application/json
User-Agent: SaveAny-Bot/1.0

Callback request body:

{
  "task_id":      "abc123xyz",
  "type":         "directlinks",
  "status":       "completed",
  "storage_id":   1,
  "storage_name": "local",
  "path":         "downloads",
  "completed_at": "2026-03-11T10:01:00Z",
  "error":        ""
}

completed_at is only present when status is completed, partial, or failed. error is only present when non-empty.

Retry policy: Up to 3 attempts. A non-2xx response or a transport error triggers a retry after 100ms, then 400ms. Each request has a 30-second timeout.