Self-hosted Python FastAPI service for programmatic video rendering. Accepts JSON timeline compositions and renders video via Editly, native FFmpeg, or HyperFrames for HTML-backed compositions. Also supports reusable templates, webhook callbacks, and deterministic render artifacts. A self-hosted, open-source alternative to Creatomate and JSON2Video.
The fastest way to run VidAPI with the full async render pipeline:
# Prerequisites: Docker Engine 24+ and Docker Compose v2
# Start API, worker, and Redis
docker compose up --build
# Verify health (in another terminal)
curl http://localhost:8000/v1/health
# Run the end-to-end smoke test
bash scripts/smoke-test.shThis starts three services:
- api (port 8000) -- FastAPI server accepting render requests
- worker -- ARQ consumer with Editly, HyperFrames, and FFmpeg for rendering
- redis (port 6379) -- Job queue broker
For a production-like local stack with API, worker, Redis AUTH, PostgreSQL, and MinIO:
cp .env.production.example .env.production
# Replace change-me values and API_KEY_HASHES before use.
docker compose --env-file .env.production -f docker-compose.prod.yml up --buildDefault environment values live in .env.docker. Override any variable:
# Example: enable debug logging
echo "LOG_LEVEL=DEBUG" >> .env.docker
docker compose up --builddocker compose down # Stop services
docker compose down -v # Stop and remove volumes# Prerequisites: Python 3.11+, Node.js 22+, FFmpeg 6+, Redis
# Starts Redis, FastAPI, and the ARQ worker on http://127.0.0.1:8005
scripts/dev.sh
# Verify
curl http://127.0.0.1:8005/v1/healthLocal development starts with API key auth disabled. The generated OpenAPI
schema reflects the active auth mode: local docs omit API-key requirements when
API_KEY_AUTH_ENABLED=false, and show X-API-Key requirements when auth is
enabled. To exercise protected API behavior locally, generate a SHA-256 hash for
a raw key and enable auth:
export VIDAPI_API_KEY="replace-with-a-local-secret"
export API_KEY_AUTH_ENABLED=true
export API_KEY_HASHES="$(python -c 'import hashlib, os; print(hashlib.sha256(os.environ["VIDAPI_API_KEY"].encode()).hexdigest())')"
scripts/dev.sh
curl -H "X-API-Key: $VIDAPI_API_KEY" http://127.0.0.1:8005/v1/renders- Python 3.11+
- Node.js 22+ (for Editly and HyperFrames renderers)
- FFmpeg 6+ and ffprobe
- Bundled fonts: Inter, Noto Sans, DejaVu
- Docker Engine 24+ and Docker Compose v2 (for containerized stack)
.
|-- app/
| |-- api/ # Route handlers, dependencies, error handling
| |-- core/ # Config (pydantic-settings), logging, security
| |-- db/ # SQLModel tables, CRUD, async sessions
| |-- models/ # Pydantic composition, template, and render schemas
| |-- renderers/ # Renderer protocol, capabilities, Editly, HyperFrames, FFmpeg
| |-- services/ # Render pipeline, asset, template, and webhook services
| |-- storage/ # Storage protocol and local filesystem adapter
| \-- workers/ # Background job workers (Phase 01)
|-- alembic/ # Database migrations
|-- tests/ # Full schema, security, compiler, API, worker, and integration coverage
|-- docs/ # Architecture, development, deployment guides
|-- Dockerfile.api # Slim API image (Python + FastAPI)
|-- Dockerfile.worker # Full worker image (Python + Node + FFmpeg)
|-- docker-compose.yml # Multi-service compose stack (API + Worker + Redis)
|-- scripts/ # Docker helper scripts (health check, smoke test)
\-- pyproject.toml # Dependencies and tool config
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/health |
Health check (includes Redis status in async mode) |
POST |
/v1/renders |
Create a render job (returns 202 in async mode) |
GET |
/v1/renders |
List recent renders with pagination |
GET |
/v1/renders/{id} |
Get render status, progress, and output URLs |
DELETE |
/v1/renders/{id} |
Cancel a queued or running render |
GET |
/v1/renders/{id}/download |
Download rendered output |
HEAD |
/v1/renders/{id}/download |
Inspect output headers without downloading bytes |
GET |
/v1/renders/{id}/poster |
Download or redirect to a render poster |
GET |
/v1/renders/{id}/captions |
Download or redirect to a caption sidecar |
GET |
/v1/renders/{id}/artifacts/{name} |
Download deterministic artifacts such as input.json, expanded.json, compiled.editly.json, replay.json, logs.txt, poster.jpg, output, or manifest.json |
Operational endpoints are mounted under /v1/ops and require X-API-Key when
API key auth is enabled.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/ops/renders |
Recent renders with pagination and optional status filter |
GET |
/v1/ops/renders/failures |
Recent failed renders with redacted error excerpts |
GET |
/v1/ops/renders/status-counts |
Current render counts by status |
GET |
/v1/ops/renders/renderer-failures |
Failed render counts by renderer and error code |
GET |
/v1/ops/webhooks |
Recent webhook attempts with optional render filter |
GET |
/v1/ops/webhooks/outcome-counts |
Webhook outcomes by event |
GET |
/v1/ops/metrics |
Prometheus-style metrics text |
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/templates |
Create a reusable template |
GET |
/v1/templates |
List templates |
GET |
/v1/templates/{id} |
Retrieve template metadata and active version |
PUT |
/v1/templates/{id} |
Update a template by creating a new version |
DELETE |
/v1/templates/{id} |
Soft-delete or archive a template |
POST |
/v1/templates/{id}/renders |
Render a template with merge variables |
Interactive API docs are available at /docs (Swagger) and /redoc. FastAPI's
default docs UI loads Swagger/ReDoc assets from public CDNs, so use
/openapi.json directly when working offline or behind a firewall that blocks
those assets.
For zero-state local retesting, stop the local stack and run:
scripts/dev.sh clean-stateRender requests may omit renderer, set it to auto, explicitly request
editly, explicitly request ffmpeg-native, or explicitly request
hyperframes. Omitted, null, and auto select HyperFrames when any clip uses
asset.type: "html"; otherwise they select Editly. ffmpeg-native selects the
constrained native FFmpeg adapter for simple high-throughput timelines.
hyperframes compiles VidAPI HTML assets into a workspace-local HyperFrames
project and produces an MP4 intermediate for shared finishing.
VidAPI validates renderer capabilities before direct render jobs are persisted,
queued, or compiled. Unsupported renderers return UNSUPPORTED_RENDERER.
Unsupported renderer-feature combinations return UNSUPPORTED_RENDERER_FEATURE
with bounded context. See
Renderer Capabilities for the support matrix
and extension contract. See
Native FFmpeg Renderer for the native subset,
replay artifacts, and rejection behavior. See
HyperFrames Renderer for HTML asset boundaries,
runtime dependencies, and replay artifacts.
Clip transitions are declared in VidAPI's renderer-neutral composition schema.
Supported values include fade_in, fade_out, crossfade,
directional_left, directional_right, directional_up, directional_down,
wipe_left, wipe_right, wipe_up, wipe_down, cross_zoom,
simple_zoom, circle_open, and linear_blur.
between transitions are declared on the outgoing clip and require an exact
same-track successor. Gaps, overlaps, overlong durations, audio-only clips, and
multiple transitions at one rendered boundary are rejected before the job is
queued or compiled.
{
"timeline": {
"tracks": [
{
"clips": [
{
"asset": {"type": "video", "src": "intro.mp4"},
"start": 0,
"length": 2,
"transition": {"name": "wipe_left", "duration": 0.4}
},
{
"asset": {"type": "video", "src": "main.mp4"},
"start": 2,
"length": 3
}
]
}
]
}
}See Transitions for aliases, placement rules, timing constraints, and renderer support notes.
output.format supports mp4, webm, gif, and png-sequence. Editly,
HyperFrames, and the native FFmpeg renderer all produce MP4 intermediates; WebM,
GIF, and PNG sequence outputs are finished with FFmpeg before storage. PNG
sequence downloads are zip archives and expose manifest.json through the
render artifact endpoint.
output.preset supports tiktok, reels, shorts, youtube, square-ad,
and preview-low. Explicit output.width and output.height override preset
dimensions, while omitted FPS and quality values use preset defaults.
Render requests may include a top-level captions block with timed cues.
Supported modes are sidecar for SRT or WebVTT files and burn-in for
FFmpeg-backed caption burn-in before output conversion.
Poster generation is configured under output.poster. Omitting the block keeps
the service default poster behavior. Supported modes are default,
timestamp, percent, and disabled.
{
"captions": {
"mode": "sidecar",
"format": "srt",
"cues": [{"start": 0, "duration": 1.5, "text": "Hello"}]
},
"output": {
"format": "mp4",
"poster": {"mode": "timestamp", "timestamp": 0.5}
}
}Succeeded status responses and webhook payloads can include structured
captions and poster_metadata fields. See
Captions and Posters for details.
GET /health and GET /v1/health are always public. When
API_KEY_AUTH_ENABLED=true, all render and template endpoints require an
X-API-Key header. Configure accepted keys as SHA-256 hex digests through
API_KEY_HASHES; multiple hashes can be comma-separated.
Production startup requires API key auth to be enabled and at least one hash to be configured. Raw API keys are never configured in VidAPI settings.
VidAPI rejects over-limit work before it is persisted or queued. The main local settings are:
MAX_RENDER_REQUEST_BODY_BYTES=2097152
MAX_TEMPLATE_REQUEST_BODY_BYTES=2097152
MAX_RENDER_DURATION_SECONDS=120
MAX_OUTPUT_WIDTH=1920
MAX_OUTPUT_HEIGHT=1920
MAX_FPS=60
MAX_GIF_DURATION_SECONDS=15
MAX_GIF_FPS=30
MAX_PNG_SEQUENCE_DURATION_SECONDS=10
MAX_PNG_SEQUENCE_FPS=30
MAX_PNG_SEQUENCE_FRAMES=300
MAX_TRACKS_PER_RENDER=50
MAX_CLIPS_PER_RENDER=50
MAX_ASSETS_PER_RENDER=100
MAX_ASYNC_QUEUE_DEPTH=1000Oversized HTTP bodies return 413 with REQUEST_BODY_TOO_LARGE. Over-limit
compositions or media metadata return 422 with COMPOSITION_LIMIT_EXCEEDED or
MEDIA_LIMIT_EXCEEDED. Saturated async queues return 429 with QUEUE_SATURATED
and a Retry-After header.
- Getting Started
- Development Guide
- Architecture
- Renderer Capabilities
- HyperFrames Renderer
- Transitions
- Output Formats
- Captions and Posters
- Deployment
- Environments
- Operations
- Contributing
- FastAPI - Async web framework with auto OpenAPI docs
- Pydantic v2 - Discriminated unions for composition schema validation
- ARQ + Redis - Async job queue for render workers
- SQLModel + aiosqlite/asyncpg - Async database (SQLite dev, PostgreSQL prod)
- Alembic - Database migrations
- Editly - Default video renderer (Node.js subprocess)
- HyperFrames - HTML/CSS/GSAP renderer adapter (Node.js subprocess)
- FFmpeg - Video encoding, poster extraction, audio mixing, media probing
- Pillow - Text-to-image rendering with bundled fonts
- Jinja2 - Sandboxed template expansion for reusable compositions
- httpx - Async asset downloads with SSRF protection
- structlog - Structured JSON logging
- S3-compatible storage - Production artifact backend with MinIO validation