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 withAuthorization: Bearer <token>for the API.Web listen address: shared by the console and API; use0.0.0.0:28080when 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 Code | HTTP Status | Meaning |
|---|---|---|
unauthorized | 401 | Authentication failed |
invalid_request | 400 | Malformed request body or parameters |
task_creation_failed | 400 | Failed to create task |
task_not_found | 404 | Task ID does not exist |
cancel_failed | 500 | Failed to cancel task |
internal_error | 500 | Internal 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": { }
}
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Task type — see below |
storage_id | uint | Yes | Target storage ID |
path | string | No | Subdirectory path within the storage |
webhook | string | No | Callback URL invoked when the task reaches a terminal state |
params | object | Yes | Type-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 #
directlinks — Direct URL Download #
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 field | Type | Required | Description |
|---|---|---|---|
urls | []string | Yes | List 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 field | Type | Required | Description |
|---|---|---|---|
urls | []string | Yes | List of media URLs, at least 1 |
flags | []string | No | Extra 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 field | Type | Required | Description |
|---|---|---|---|
urls | []string | Yes | List of download URIs, at least 1 |
options | map[string]string | No | Aria2 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 field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The 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 grouphttps://t.me/c/123456789/123— private channel by numeric IDhttps://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 field | Type | Required | Description |
|---|---|---|---|
message_links | []string | No | List of Telegram message links; required unless chat_id and message_range are used |
chat_id | string | No | Bot API chat ID or username used with message_range |
message_range | string | No | Inclusive message range, for example 100-120 |
filter | string | No | Optional 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 field | Type | Required | Description |
|---|---|---|---|
telegraph_url | string | Yes | URL 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 field | Type | Required | Description |
|---|---|---|---|
source_storage_id | uint | Yes | Source storage ID |
source_path | string | Yes | Path within the source storage; must contain at least one file |
target_storage_id | uint | Yes | Target storage ID |
target_path | string | Yes | Destination 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 path404 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 path404 task_not_found— task does not exist404 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 running409 nothing_to_retry— the task has no failed or unfinished files409 storage_missing/409 source_unavailable400 retry_unsupported— the task type cannot be retried503 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 terminal404 task_not_found— task does not exist500 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 it404 task_not_found— task does not exist
Task Statuses #
| Status | Meaning |
|---|---|
queued | Task is queued and waiting to run |
running | Task is currently executing |
completed | Task finished successfully |
partial | Task finished, but one or more files failed |
failed | Task encountered an error |
cancelled | Task 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.