HFDownloader REST API Reference
January 30, 2026 · View on GitHub
Complete REST API documentation for hfdownloader serve.
Table of Contents
Overview
Start the server:
hfdownloader serve --port 8080
The server provides:
- REST API for download management
- WebSocket for real-time progress updates
- Web UI at the root URL
Content Types
- Request bodies:
application/json - Response bodies:
application/json - WebSocket messages: JSON
Authentication
Basic Authentication (Optional)
Enable with server flags:
hfdownloader serve --auth-user admin --auth-pass secret123
All requests require the Authorization header:
Authorization: Basic YWRtaW46c2VjcmV0MTIz
HuggingFace Token
For private/gated models, provide via:
- Server flag:
hfdownloader serve -t hf_xxxxx - Settings API:
POST /api/settings
Base URL
http://localhost:8080/api
Endpoints
Health Check
Check server status.
GET /api/health
Response 200 OK
{
"status": "ok",
"version": "3.0.0",
"time": "2024-01-15T10:30:00Z"
}
Example
curl http://localhost:8080/api/health
Downloads
POST /api/download
Start a new download job.
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
repo | string | Yes | Repository ID (owner/name) | |
revision | string | No | main | Branch, tag, or commit |
dataset | boolean | No | false | Treat as dataset |
filters | string[] | No | [] | File filter patterns |
excludes | string[] | No | [] | Exclude patterns |
appendFilterSubdir | boolean | No | false | Create filter subdirs |
dryRun | boolean | No | false | Plan only |
Filter Syntax
Filters can be embedded in repo name:
{ "repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF:q4_k_m,q5_k_m" }
Or as separate field:
{
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"filters": ["q4_k_m", "q5_k_m"]
}
Response 202 Accepted (New job)
{
"id": "a1b2c3d4e5f6",
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"revision": "main",
"isDataset": false,
"filters": ["q4_k_m"],
"excludes": [],
"outputDir": "/home/user/.cache/huggingface/hub",
"status": "queued",
"progress": {
"totalFiles": 0,
"completedFiles": 0,
"totalBytes": 0,
"downloadedBytes": 0,
"bytesPerSecond": 0
},
"error": "",
"createdAt": "2024-01-15T10:30:00Z",
"startedAt": null,
"endedAt": null,
"files": []
}
Response 200 OK (Existing job)
{
"job": { /* job object */ },
"message": "Download already in progress"
}
Examples
# Basic download
curl -X POST http://localhost:8080/api/download \
-H "Content-Type: application/json" \
-d '{"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF"}'
# With filters
curl -X POST http://localhost:8080/api/download \
-H "Content-Type: application/json" \
-d '{
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"filters": ["q4_k_m", "q5_k_m"]
}'
# Dataset download
curl -X POST http://localhost:8080/api/download \
-H "Content-Type: application/json" \
-d '{"repo": "facebook/flores", "dataset": true}'
# Specific revision
curl -X POST http://localhost:8080/api/download \
-H "Content-Type: application/json" \
-d '{"repo": "CompVis/stable-diffusion-v1-4", "revision": "fp16"}'
POST /api/plan
Get download plan without starting download.
Request Body
Same as /api/download.
Response 200 OK
{
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"revision": "main",
"files": [
{
"path": "config.json",
"size": 1024,
"lfs": false
},
{
"path": "mistral-7b.Q4_K_M.gguf",
"size": 4368438272,
"lfs": true
}
],
"totalSize": 4368439296,
"totalFiles": 2
}
Example
curl -X POST http://localhost:8080/api/plan \
-H "Content-Type: application/json" \
-d '{"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF", "filters": ["q4_k_m"]}'
Jobs
GET /api/jobs
List all download jobs.
Response 200 OK
{
"jobs": [
{
"id": "a1b2c3d4e5f6",
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"revision": "main",
"isDataset": false,
"filters": ["q4_k_m"],
"excludes": [],
"outputDir": "/home/user/.cache/huggingface/hub",
"status": "running",
"progress": {
"totalFiles": 3,
"completedFiles": 1,
"totalBytes": 4500000000,
"downloadedBytes": 1500000000,
"bytesPerSecond": 50000000
},
"error": "",
"createdAt": "2024-01-15T10:30:00Z",
"startedAt": "2024-01-15T10:30:01Z",
"endedAt": null,
"files": [
{
"path": "config.json",
"totalBytes": 1024,
"downloaded": 1024,
"status": "complete"
},
{
"path": "mistral-7b.Q4_K_M.gguf",
"totalBytes": 4368438272,
"downloaded": 1500000000,
"status": "downloading"
}
]
}
],
"count": 1
}
Example
curl http://localhost:8080/api/jobs
GET /api/jobs/{id}
Get specific job details.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Job ID |
Response 200 OK
{
"id": "a1b2c3d4e5f6",
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"status": "running",
/* ... full job object ... */
}
Response 404 Not Found
{
"error": "Job not found"
}
Example
curl http://localhost:8080/api/jobs/a1b2c3d4e5f6
DELETE /api/jobs/{id}
Cancel a running or queued job.
Response 200 OK
{
"success": true,
"message": "Job cancelled"
}
Response 404 Not Found
{
"error": "Job not found or already completed"
}
Example
curl -X DELETE http://localhost:8080/api/jobs/a1b2c3d4e5f6
POST /api/jobs/{id}/pause
Pause a running job.
Response 200 OK
{
"success": true,
"message": "Job paused"
}
Response 404 Not Found
{
"error": "Job not found or not running"
}
Example
curl -X POST http://localhost:8080/api/jobs/a1b2c3d4e5f6/pause
POST /api/jobs/{id}/resume
Resume a paused job.
Response 200 OK
{
"success": true,
"message": "Job resumed"
}
Response 404 Not Found
{
"error": "Job not found or not paused"
}
Note: Resumed jobs restart from queued status. Progress is reset, but already-downloaded files are automatically skipped.
Example
curl -X POST http://localhost:8080/api/jobs/a1b2c3d4e5f6/resume
Job Status Lifecycle
queued ─────► running ─────► completed
│
├─────► failed
│
├─────► cancelled
│
└─────► paused ─────► queued ─► running ─► ...
| Status | Description |
|---|---|
queued | Waiting to start |
running | Download in progress |
paused | Paused by user |
completed | Finished successfully |
failed | Error occurred |
cancelled | Cancelled by user |
Settings
GET /api/settings
Get current server settings.
Response 200 OK
{
"token": "********mnop",
"cacheDir": "/home/user/.cache/huggingface/hub",
"connections": 8,
"maxActive": 3,
"multipartThreshold": "32MiB",
"verify": "size",
"retries": 4,
"endpoint": ""
}
Note: Token is masked for security (shows ******** + last 4 characters).
Example
curl http://localhost:8080/api/settings
POST /api/settings
Update server settings.
Request Body
| Field | Type | Description |
|---|---|---|
token | string | HuggingFace token |
connections | integer | Connections per file |
maxActive | integer | Max concurrent downloads |
multipartThreshold | string | Min size for multipart |
verify | string | Verification: none, size, sha256 |
retries | integer | Retry attempts |
Security Restrictions
cacheDircannot be changed via APImodelsDircannot be changed via APIdatasetsDircannot be changed via API
Response 200 OK
{
"success": true,
"message": "Settings updated"
}
Example
curl -X POST http://localhost:8080/api/settings \
-H "Content-Type: application/json" \
-d '{
"token": "hf_xxxxx",
"connections": 16,
"maxActive": 8
}'
Analyzer
GET /api/analyze/{repo}
Analyze a HuggingFace repository.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
repo | string | Repository ID (owner/name) |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
dataset | boolean | Force dataset type |
revision | string | Branch/tag to analyze |
Response 200 OK (Determined type)
{
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"is_dataset": false,
"type": "gguf",
"type_description": "GGUF Model",
"file_count": 12,
"total_size": 4500000000,
"total_size_human": "4.2 GiB",
"branch": "main",
"refs": [
{"name": "main", "type": "branch", "commit": "abc123..."}
],
"files": [
{
"name": "config.json",
"path": "config.json",
"size": 1024,
"lfs": false
},
{
"name": "mistral-7b.Q4_K_M.gguf",
"path": "mistral-7b.Q4_K_M.gguf",
"size": 4368438272,
"lfs": true,
"sha256": "abc123..."
}
],
"gguf": {
"model_name": "Mistral-7B-Instruct-v0.2",
"quantizations": [
{
"name": "Q4_K_M",
"file": { /* FileInfo */ },
"quality": 4,
"quality_stars": "★★★★☆",
"estimated_ram": 4905066496,
"estimated_ram_human": "4.6 GiB",
"description": "Good balance of quality and size"
}
]
},
"analyzed_at": "2024-01-15T10:30:00Z"
}
Response 200 OK (Needs selection)
When a repo exists as both model and dataset:
{
"needsSelection": true,
"repo": "owner/name",
"message": "This repository exists as both a model and a dataset. Please select which one you want to analyze.",
"options": ["model", "dataset"]
}
Examples
# Analyze model
curl http://localhost:8080/api/analyze/TheBloke/Mistral-7B-Instruct-v0.2-GGUF
# Force dataset
curl "http://localhost:8080/api/analyze/facebook/flores?dataset=true"
# Specific revision
curl "http://localhost:8080/api/analyze/owner/repo?revision=v1.0"
Detected Model Types
| Type | Description | Key Fields |
|---|---|---|
gguf | GGUF quantized model | gguf.quantizations |
transformers | Transformers model | transformers.architecture |
diffusers | Diffusers pipeline | diffusers.pipeline_type |
lora | LoRA adapter | lora.base_model |
gptq | GPTQ quantized | gptq.bits |
awq | AWQ quantized | awq.bits |
onnx | ONNX model | onnx.models |
audio | Audio model | audio.task |
vision | Vision model | vision.task |
multimodal | Multimodal model | multimodal.modalities |
dataset | Dataset | dataset.formats |
Cache
GET /api/cache
List cached repositories.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
type | string | Filter: model, dataset |
Response 200 OK
{
"repos": [
{
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"type": "model",
"path": "/home/user/.cache/huggingface/hub/models--TheBloke--Mistral-7B-GGUF"
},
{
"repo": "facebook/flores",
"type": "dataset",
"path": "/home/user/.cache/huggingface/hub/datasets--facebook--flores"
}
],
"count": 2,
"cacheDir": "/home/user/.cache/huggingface/hub"
}
Examples
# List all
curl http://localhost:8080/api/cache
# Models only
curl "http://localhost:8080/api/cache?type=model"
GET /api/cache/{repo}
Get cached repository details.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
repo | string | Repository ID (owner/name) |
Response 200 OK
{
"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF",
"type": "model",
"path": "/home/user/.cache/huggingface/hub/models--TheBloke--Mistral-7B-GGUF",
"snapshots": ["main", "v1.0"]
}
Response 404 Not Found
{
"error": "Repository not found in cache"
}
Example
curl http://localhost:8080/api/cache/TheBloke/Mistral-7B-Instruct-v0.2-GGUF
WebSocket API
Real-time updates via WebSocket connection.
Connection
ws://localhost:8080/api/ws
Connection Parameters
| Parameter | Value |
|---|---|
| Read buffer | 1024 bytes |
| Write buffer | 1024 bytes |
| Max message | 512 KB |
| Ping interval | 30 seconds |
| Read timeout | 60 seconds |
| Write timeout | 10 seconds |
Message Format
All messages are JSON:
{
"type": "message_type",
"data": { /* payload */ }
}
Message Types
init
Sent immediately upon connection.
{
"type": "init",
"data": {
"jobs": [ /* all current jobs */ ],
"version": "3.0.0"
}
}
job_update
Broadcast when job status changes.
{
"type": "job_update",
"data": {
"id": "a1b2c3d4e5f6",
"repo": "owner/model",
"status": "running",
"progress": {
"totalFiles": 5,
"completedFiles": 2,
"totalBytes": 5000000000,
"downloadedBytes": 2000000000,
"bytesPerSecond": 50000000
},
"files": [
{
"path": "model.bin",
"totalBytes": 5000000000,
"downloaded": 2000000000,
"status": "downloading"
}
]
}
}
event
General event notifications.
{
"type": "event",
"data": {
"event": "download_complete",
"jobId": "a1b2c3d4e5f6"
}
}
JavaScript Example
const ws = new WebSocket('ws://localhost:8080/api/ws');
ws.onopen = () => {
console.log('Connected');
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'init':
console.log('Jobs:', msg.data.jobs);
break;
case 'job_update':
const job = msg.data;
const percent = (job.progress.downloadedBytes / job.progress.totalBytes * 100).toFixed(1);
console.log(`${job.repo}: ${percent}%`);
break;
}
};
ws.onerror = (err) => console.error('WebSocket error:', err);
ws.onclose = () => console.log('Disconnected');
Python Example
import asyncio
import websockets
import json
async def monitor():
uri = "ws://localhost:8080/api/ws"
async with websockets.connect(uri) as ws:
async for message in ws:
msg = json.loads(message)
if msg["type"] == "job_update":
job = msg["data"]
progress = job["progress"]
if progress["totalBytes"] > 0:
pct = progress["downloadedBytes"] / progress["totalBytes"] * 100
print(f"{job['repo']}: {pct:.1f}%")
asyncio.run(monitor())
Error Handling
Error Response Format
{
"error": "Error message",
"details": "Additional details (optional)"
}
HTTP Status Codes
| Code | Meaning | Usage |
|---|---|---|
| 200 | OK | Successful GET, existing job |
| 202 | Accepted | New job created |
| 204 | No Content | CORS preflight |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Auth required/failed |
| 404 | Not Found | Resource not found |
| 500 | Server Error | Internal error |
Common Errors
Invalid Repository
{
"error": "Invalid repo format",
"details": "Expected owner/name"
}
Job Not Found
{
"error": "Job not found"
}
Analysis Failed
{
"error": "Analysis failed",
"details": "404: repository not found"
}
Examples
Complete Download Workflow
# 1. Analyze repository
curl http://localhost:8080/api/analyze/TheBloke/Mistral-7B-Instruct-v0.2-GGUF
# 2. Start download with specific quantization
curl -X POST http://localhost:8080/api/download \
-H "Content-Type: application/json" \
-d '{"repo": "TheBloke/Mistral-7B-Instruct-v0.2-GGUF", "filters": ["q4_k_m"]}'
# 3. Monitor progress
curl http://localhost:8080/api/jobs
# 4. Get specific job
curl http://localhost:8080/api/jobs/a1b2c3d4e5f6
Pause/Resume Workflow
# Start download
JOB_ID=$(curl -s -X POST http://localhost:8080/api/download \
-H "Content-Type: application/json" \
-d '{"repo": "owner/large-model"}' | jq -r '.id')
# Pause
curl -X POST "http://localhost:8080/api/jobs/$JOB_ID/pause"
# Resume later
curl -X POST "http://localhost:8080/api/jobs/$JOB_ID/resume"
Real-time Monitoring Script
#!/bin/bash
# monitor.sh - Monitor download progress
JOB_ID=\$1
while true; do
STATUS=$(curl -s "http://localhost:8080/api/jobs/$JOB_ID")
JOB_STATUS=$(echo "$STATUS" | jq -r '.status')
if [ "$JOB_STATUS" = "completed" ] || [ "$JOB_STATUS" = "failed" ]; then
echo "Job $JOB_STATUS"
break
fi
DOWNLOADED=$(echo "$STATUS" | jq '.progress.downloadedBytes')
TOTAL=$(echo "$STATUS" | jq '.progress.totalBytes')
if [ "$TOTAL" -gt 0 ]; then
PCT=$(echo "scale=1; $DOWNLOADED * 100 / $TOTAL" | bc)
echo "Progress: $PCT%"
fi
sleep 2
done
Integration with jq
# Get all running jobs
curl -s http://localhost:8080/api/jobs | jq '.jobs[] | select(.status == "running")'
# Get total download speed
curl -s http://localhost:8080/api/jobs | jq '[.jobs[].progress.bytesPerSecond] | add'
# List repos being downloaded
curl -s http://localhost:8080/api/jobs | jq -r '.jobs[].repo'
CORS
Headers
| Header | Value |
|---|---|
Access-Control-Allow-Origin | Configured origins or * |
Access-Control-Allow-Methods | GET, POST, PUT, DELETE, OPTIONS |
Access-Control-Allow-Headers | Content-Type, Authorization |
Access-Control-Max-Age | 86400 |
Configuration
CORS origins are configured via server settings (code modification required for custom origins).
Rate Limiting
No rate limiting is implemented. For production deployments, consider using a reverse proxy (nginx, Caddy) with rate limiting.
Security Considerations
- Token Protection: HF token is masked in API responses
- Directory Lock: Output directories cannot be changed via API
- Basic Auth: Optional authentication for all endpoints
- CORS: Configurable origin restrictions
- Input Validation: Repository format validation
- File Size Limits: WebSocket messages limited to 512 KB