Open Source REST API for video processing with FFmpeg. Create vertical Shorts, add subtitles, cut videos, extract audio.
English | Русский
expires_at field shows exact deletion time (ISO 8601)docker pull alexbic/video-processor-api:latest
docker run -d -p 5001:5001 \
-v $(pwd)/tasks:/app/tasks \
--name video-processor \
alexbic/video-processor-api:latest
Public version includes:
docker-compose up -d
Configuration:
./tasks:/app/tasks (task storage)API supports smart dual-mode operation with Bearer token authentication:
🔑 Two Operation Modes:
1️⃣ Public API Mode (when BOTH API_KEY AND PUBLIC_BASE_URL are set):
PUBLIC_BASE_URL2️⃣ Internal Docker Network Mode (when API_KEY or PUBLIC_BASE_URL is NOT set):
http://video-processor:5001)API_KEY set, or only PUBLIC_BASE_URL setSetup:
# Generate secure API key
openssl rand -hex 32
# Public API mode (requires authentication) - BOTH parameters required
export API_KEY="your-generated-key-here"
export PUBLIC_BASE_URL="https://your-domain.com/video-api"
# Internal Docker mode (no authentication) - unset both or either
unset API_KEY
unset PUBLIC_BASE_URL
Usage with API Key:
curl -H "Authorization: Bearer your-api-key" \
-X POST http://localhost:5001/process_video \
-H "Content-Type: application/json" \
-d '{"video_url": "...", "operations": [...]}'
Endpoint Protection:
/health, /fonts, /task_status/{task_id}, /download/{task_id}/...API_KEY and PUBLIC_BASE_URL are set): /process_video, /taskstask_id acts as temporary access token (UUID, 72h TTL)Add a client_meta field (any JSON object) to your request, and it will be:
metadata.json,/process_video (sync) and /task_status/{task_id} (async) responses,task_completed/task_failed).This is useful for titles/captions for different social networks, campaign IDs, trace-ids, etc.
Example request with client_meta:
{
"video_url": "https://example.com/video.mp4",
"execution": "async",
"operations": [{"type": "make_short", "crop_mode": "letterbox"}],
"client_meta": {
"titles": {
"tiktok": "Cool AI Video",
"youtube": "Amazing AI Demo",
"instagram": "AI in Action"
},
"campaign_id": "cmp-2025-11-13"
}
}
In responses, the field will be available as client_meta unchanged.
Limits (to protect the service):
16 KB (UTF‑8 JSON)5 levels2002001000 charsCompatibility: client_meta may also be sent as a JSON string (it will be parsed server-side). Prefer sending an object directly.
n8n tip: if you have a nested object available only via string, you can send it using toJsonString() and the API will parse nested JSON strings too. Example:
{
"client_meta": {
"metadata": {{ $json.metadata.toJsonString() }}
}
}
The server will convert metadata from a JSON string to an object before validation and saving.
Immediate echo:
client_meta is included in the final response.client_meta is included immediately in the 202 response (along with task_id and check_status_url).GET /health — service status (versions, storage_mode, Redis availability) [no authorization]GET /fonts — list of available fonts (10 fonts in public version) [no authorization]POST /process_video — make_short, cut_video, extract_audio (sync/async, webhooks) [requires API key in Public mode only]GET /task_status/{task_id} — task status (queued/processing/completed/error) [no authorization]GET /tasks — recent tasks (for debugging) [requires API key in Public mode only]GET /download/{task_id}/{filename} — download completed file [no authorization]GET /download/{task_id}/metadata.json — result metadata [no authorization]curl http://localhost:5001/health
Response:
{
"status": "healthy",
"service": "video-processor-api",
"storage_mode": "redis",
"redis_available": true,
"api_key_enabled": true,
"timestamp": "2025-01-08T10:00:00"
}
curl http://localhost:5001/fonts
Response:
{
"status": "success",
"total_fonts": 10,
"fonts": [
{
"name": "Charter",
"filename": "Charter.ttc",
"file": "/app/fonts/Charter.ttc",
"type": "ttc"
},
{
"name": "Copperplate",
"filename": "Copperplate.ttc",
"file": "/app/fonts/Copperplate.ttc",
"type": "ttc"
},
...
],
"note": "These are the custom fonts available for video generation in /app/fonts/"
}
Available Fonts (Public Version):
GET /fonts to see the complete listSee FONTS.md for font details and examples.
POST /process_video
Request structure:
{
"video_url": "https://example.com/video.mp4",
"execution": "sync|async",
"operations": [{"type": "make_short|cut_video|extract_audio", ...}],
"webhook": {"url": "...", "headers": {...}},
"client_meta": {...}
}
Available operations:
cut_video - cut video by timecodesmake_short - convert to Shorts format with text overlays (max 2 text items in public version)extract_audio - extract audio track with automatic chunking for Whisper APISee 📖 Examples section below for detailed usage examples.
Unified format - all operations return the same structure:
{
"task_id": "abc123",
"status": "completed",
"created_at": "2025-01-08T10:05:18",
"completed_at": "2025-01-08T10:05:23",
"input": {
"video_url": "https://example.com/video.mp4",
"operations": [
{
"operation": "make_short",
"title": "Amazing Video",
"font": "Montserrat-Bold.ttf"
}
],
"operations_count": 1
},
"output": {
"output_files": [
{
"filename": "short_20251116_210049.mp4",
"file_size": 16040960,
"file_size_mb": 15.3,
"download_url": "http://video-processor:5001/download/abc123/short_20251116_210049.mp4",
"download_path": "/download/abc123/short_20251116_210049.mp4"
},
{
"filename": "short_20251116_210049_thumbnail.jpg",
"file_size": 211762,
"file_size_mb": 0.2,
"download_url": "http://video-processor:5001/download/abc123/short_20251116_210049_thumbnail.jpg",
"download_path": "/download/abc123/short_20251116_210049_thumbnail.jpg"
}
],
"total_files": 2,
"total_size": 16252722,
"total_size_mb": 15.5,
"is_chunked": false,
"metadata_url": "/download/abc123/metadata.json",
"ttl_seconds": 259200,
"ttl_human": "3 days",
"expires_at": "2025-01-11T10:05:23"
}
}
Response structure:
task_id, status, timestampsinput: Original request data (video_url, operations)output: Processing results (output_files, total_files, metadata URL, TTL info)output_files is always an array (even if 1 file)is_chunked: true if files are split into chunks (for Whisper API)All errors are returned with an HTTP code, status: "error" field and message in error.
{ "status": "error", "error": "video_url is required" }
{ "status": "error", "error": "Task not found" }
{ "status": "error", "error": "Invalid file path" }
{ "status": "error", "error": "File not found" }
{ "status": "error", "error": "FFmpeg error: ..." }
In webhooks on error, event remains event: "task_failed", and status is status: "error".
For chunked files (extract_audio with splitting):
{
"output_files": [
{"filename": "audio_chunk_000.mp3", "chunk": "1:7", ...},
{"filename": "audio_chunk_001.mp3", "chunk": "2:7", ...}
],
"is_chunked": true
}
{
"execution": "sync"
}
Response (immediately):
{
"task_id": "abc123",
"status": "completed",
"created_at": "2025-01-08T10:05:18",
"completed_at": "2025-01-08T10:05:23",
"input": {
"video_url": "https://example.com/video.mp4",
"operations": [{"operation": "cut_video", "start": 10, "end": 30}],
"operations_count": 1
},
"output": {
"output_files": [
{
"filename": "output_20250108_100523.mp4",
"file_size": 16040960,
"file_size_mb": 15.3,
"download_url": "http://video-processor:5001/download/abc123/output_20250108_100523.mp4",
"download_path": "/download/abc123/output_20250108_100523.mp4"
}
],
"total_files": 1,
"is_chunked": false,
"metadata_url": "/download/abc123/metadata.json",
"ttl_seconds": 259200,
"ttl_human": "3 days",
"expires_at": "2025-01-11T10:05:23"
}
}
{
"execution": "async"
}
Response (immediately, HTTP 202):
{
"task_id": "abc123",
"status": "queued",
"message": "Task created and processing in background",
"check_status_url": "http://video-processor:5001/task_status/abc123"
}
Check status:
curl http://localhost:5001/task_status/abc123
Response:
{
"task_id": "abc123",
"status": "completed",
"created_at": "2025-01-08T10:05:18",
"completed_at": "2025-01-08T10:05:23",
"input": {
"video_url": "https://example.com/video.mp4",
"operations": [{"operation": "cut_video", "start": 10, "end": 30}]
},
"output": {
"output_files": [
{
"filename": "output.mp4",
"file_size": 16040960,
"file_size_mb": 15.3,
"download_url": "http://video-processor:5001/download/abc123/output.mp4",
"download_path": "/download/abc123/output.mp4"
}
],
"total_files": 1,
"total_size": 16040960,
"total_size_mb": 15.3,
"is_chunked": false,
"metadata_url": "http://video-processor:5001/download/abc123/metadata.json",
"ttl_seconds": 259200,
"ttl_human": "3 days",
"expires_at": "2025-01-11T10:05:23"
}
}
Add webhook object to receive notifications on task completion:
{
"webhook": {
"url": "https://n8n.example.com/webhook/video-completed"
}
}
Custom Webhook Headers (Optional):
You can add custom headers for webhook authentication via webhook.headers:
{
"webhook": {
"url": "https://n8n.example.com/webhook/video-completed",
"headers": {
"X-API-Key": "your-secret-key",
"Authorization": "Bearer token-123"
}
}
}
Use Cases:
Validation:
Content-Type cannot be overriddenwebhook.headers in each request (global headers not supported in public version)Webhook Payload (success):
{
"task_id": "abc123",
"event": "task_completed",
"status": "completed",
"created_at": "2025-01-08T10:05:18",
"completed_at": "2025-01-08T10:05:23",
"input": {
"video_url": "https://example.com/video.mp4",
"operations": [{"operation": "cut_video", "start": 10, "end": 30}],
"operations_count": 1
},
"output": {
"output_files": [
{
"filename": "output.mp4",
"file_size": 16040960,
"file_size_mb": 15.3,
"download_url": "http://video-processor:5001/download/abc123/output.mp4",
"download_path": "/download/abc123/output.mp4"
}
],
"total_files": 1,
"total_size": 16040960,
"total_size_mb": 15.3,
"is_chunked": false,
"metadata_url": "http://video-processor:5001/download/abc123/metadata.json",
"ttl_seconds": 259200,
"ttl_human": "3 days",
"expires_at": "2025-01-11T10:05:23"
}
}
Webhook Payload (error):
{
"task_id": "abc123",
"event": "task_failed",
"status": "error",
"error": "FFmpeg error: ...",
"failed_at": "2025-01-08T10:05:23"
}
Webhook State Tracking:
Webhook delivery state is saved in metadata.json under webhook field:
{
"webhook": {
"url": "https://n8n.example.com/webhook/video-completed",
"headers": {"X-API-Key": "***"},
"status": "delivered",
"attempts": 1,
"last_attempt": "2025-01-08T10:05:23",
"last_status": 200,
"last_error": null,
"next_retry": null
}
}
Retry Logic:
pending → delivered / failedTask statuses and transitions:
queued → task created and queued (async)processing → operations executing (progress 5–95%)completed → finished; output_files, is_chunked, metadata_url, video_url availableerror → execution error; error — description, failed_at — timestampKey status fields:
task_id: task identifierstatus: queued | processing | completed | errorprogress: 0–100 (for async)created_at / completed_at / failed_at: timestampsoutput_files: always an array; when chunked contains chunk: "i:n"is_chunked: true if output_files has chunk fieldPolling recommendations:
GET /task_status/{task_id} every 2–3 secondsstatus is {completed, error}| Variable | Default | Description |
|---|---|---|
| Authentication & URLs | ||
API_KEY | — | Enables public mode (Bearer required). When unset, internal mode (no auth). |
PUBLIC_BASE_URL | — | External base for absolute URLs (https://host/app). Used only if API_KEY is set. |
INTERNAL_BASE_URL | http://video-processor:5001 | Base for background URL generation (webhooks, logs). |
LOG_LEVEL | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). |
Notes:
API_KEY set + PUBLIC_BASE_URL defined → service exposes absolute URLs and requires Bearer token.API_KEY → internal mode suitable for Docker network usage (no auth).check_status_url is always absolute in async responses.GET/POST /recover/{task_id}RECOVERY_PUBLIC_ENABLED=true (use only in trusted network)force=1 to ignore expired TTLResponse:
{ "task_id": "...", "ok": true, "message": "Recovery started", "status": "processing", "retry_count": 1 }
What it does:
{
"video_url": "https://example.com/long-video.mp4",
"execution": "sync",
"operations": [
{
"type": "make_short",
"start_time": 10.5,
"end_time": 70.0,
"crop_mode": "letterbox",
"text_items": [
{
"text": "My First Shorts",
"fontfile": "HelveticaNeue.ttc",
"fontsize": 70,
"fontcolor": "white",
"x": "(w-text_w)/2",
"y": 100,
"start": 0,
"end": 60,
"box": 1,
"boxcolor": "[email protected]"
},
{
"text": "Subscribe for more!",
"fontfile": "PTSans.ttc",
"fontsize": 48,
"fontcolor": "yellow",
"x": "(w-text_w)/2",
"y": "h-200",
"start": 0,
"end": 3
}
],
"generate_thumbnail": true
}
]
}
Note: start_time/end_time accept numbers (seconds) or strings ("00:01:30"). Time in text_items is relative to the cropped video.
What it does:
{
"video_url": "https://example.com/landscape.mp4",
"execution": "sync",
"operations": [
{
"type": "make_short",
"crop_mode": "letterbox",
"generate_thumbnail": true,
"thumbnail_timestamp": 0.5
}
]
}
What it does:
borderw: 3px black) + background box with 8px border{
"video_url": "https://example.com/video.mp4",
"execution": "sync",
"operations": [
{
"type": "make_short",
"crop_mode": "letterbox",
"text_items": [
{
"text": "Title",
"fontfile": "HelveticaNeue.ttc",
"fontsize": 80,
"fontcolor": "white",
"x": "(w-text_w)/2",
"y": 100,
"start": 0,
"end": 60,
"box": 1,
"boxcolor": "[email protected]"
},
{
"text": "",
"fontfile": "PTSans.ttc",
"fontsize": 60,
"fontcolor": "yellow",
"borderw": 3,
"bordercolor": "black",
"box": 1,
"boxcolor": "[email protected]",
"boxborderw": 8,
"x": "(w-text_w)/2",
"y": "h-200",
"subtitles": {
"items": [
{"text": "First word", "start": 0, "end": 1.5},
{"text": "Second word", "start": 1.5, "end": 3},
{"text": "Third word", "start": 3, "end": 4.5}
]
}
}
]
}
]
}
What it does:
{
"video_url": "https://example.com/long-video.mp4",
"execution": "sync",
"operations": [
{
"type": "cut_video",
"start_time": "00:01:30",
"end_time": "00:02:00"
}
]
}
What it does:
{
"video_url": "https://example.com/video.mp4",
"execution": "async",
"operations": [
{
"type": "cut_video",
"start_time": "00:00:10",
"end_time": "00:01:00"
},
{
"type": "make_short",
"letterbox_config": {"width": 1080, "height": 1920},
"title": {"text": "Episode 1", "fontsize": 70}
}
]
}
curl -X POST http://localhost:5001/process_video \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://example.com/video.mp4",
"execution": "sync",
"operations": [
{
"type": "extract_audio",
"format": "mp3",
"bitrate": "192k"
}
]
}'
Response (sync - returned immediately after completion):
{
"task_id": "abc123-def456",
"status": "completed",
"output_files": [
{
"filename": "audio_20251112_194523.mp3",
"file_size": 5048576,
"file_size_mb": 4.8,
"download_url": "http://localhost:5001/download/abc123-def456/audio_20251112_194523.mp3",
"download_path": "/download/abc123-def456/audio_20251112_194523.mp3"
}
],
"total_files": 1,
"is_chunked": false,
"metadata_url": "http://localhost:5001/download/abc123-def456/metadata.json",
"note": "Files will auto-delete after 3 days.",
"completed_at": "2025-11-12T19:45:23"
}
{
"video_url": "https://example.com/video.mp4",
"execution": "async",
"operations": [
{
"type": "extract_audio",
"format": "mp3",
"bitrate": "320k"
}
],
"webhook": {
"url": "https://n8n.example.com/webhook/audio-ready"
}
Response (async - returned immediately):
Content type
Image
Digest
sha256:7d195f66a…
Size
307.3 MB
Last updated
6 months ago
docker pull alexbic/video-processor-api