Quickstart: Python
Render a video from Python using the requests library.
Prerequisites
pip install requests
Set your credentials:
export VIRALSYNC_API_KEY="vs_prod_YOUR_API_KEY"
export VIRALSYNC_PROJECT_ID="proj_abc123"
Basic example: submit and poll
# render.py
import os
import time
import requests
API_KEY = os.environ['VIRALSYNC_API_KEY']
PROJECT_ID = os.environ['VIRALSYNC_PROJECT_ID']
BASE_URL = 'https://api.viralsync.io'
HEADERS = {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
}
def submit_render() -> str:
payload = {
'kind': 'movie',
'projectId': PROJECT_ID,
'movie': {
'width': 1920,
'height': 1080,
'fps': 30,
'scenes': [
{
'id': 'scene_1',
'duration': 5,
'bgColor': '#1a1a2e',
'layers': [
{
'id': 'title',
'type': 'text',
'text': 'Hello from Python',
'startTime': 0,
'duration': 5,
'zIndex': 1,
'position': {'x': 360, 'y': 460},
'size': {'width': 1200, 'height': 160},
'fontSize': 80,
'fontWeight': 'bold',
'color': '#00d4ff',
'textAlign': 'center',
}
],
}
],
},
}
response = requests.post(
f'{BASE_URL}/v1/render/jobs',
json=payload,
headers=HEADERS,
)
response.raise_for_status()
return response.json()['jobId']
def poll_until_done(job_id: str) -> str:
while True:
response = requests.get(
f'{BASE_URL}/v1/jobs/{job_id}',
headers=HEADERS,
)
response.raise_for_status()
job = response.json()
pct = int((job.get('progress') or 0) * 100)
phase = f" — {job['phase']}" if job.get('phase') else ''
print(f"[{job['status']}] {pct}%{phase}")
if job['status'] == 'done':
return job['outputUrl']
if job['status'] == 'failed':
raise RuntimeError(f"Render failed: {job.get('error')}")
if job['status'] == 'cancelled':
raise RuntimeError('Job was cancelled')
time.sleep(3)
def main():
print('Submitting render job...')
job_id = submit_render()
print(f'Job queued: {job_id}')
print('Waiting for completion...')
output_url = poll_until_done(job_id)
print(f'\nVideo ready: {output_url}')
if __name__ == '__main__':
main()
Download the output video
import urllib.request
def download_video(output_url: str, filename: str = 'output.mp4') -> None:
urllib.request.urlretrieve(output_url, filename)
print(f'Saved to {filename}')
output_url = poll_until_done(job_id)
download_video(output_url)
Real-time progress with SSE
For real-time progress updates, use httpx with streaming (or sseclient-py):
pip install httpx sseclient-py
import json
import httpx
import sseclient
def stream_progress(job_id: str) -> str:
url = f'{BASE_URL}/v1/jobs/{job_id}/progress'
with httpx.stream('GET', url, headers={'x-api-key': API_KEY}) as response:
client = sseclient.SSEClient(response.iter_bytes())
for event in client.events():
if not event.data:
continue
update = json.loads(event.data)
pct = int((update.get('progress') or 0) * 100)
print(f"\r[{update['status']}] {pct}%", end='', flush=True)
if update['status'] == 'done':
print()
return update['outputUrl']
if update['status'] in ('failed', 'cancelled'):
print()
raise RuntimeError(f"Job ended with status: {update['status']}")
Error handling with retry
import time
import requests
from requests.exceptions import HTTPError
def submit_render_with_retry(payload: dict, max_attempts: int = 3) -> str:
for attempt in range(1, max_attempts + 1):
response = requests.post(
f'{BASE_URL}/v1/render/jobs',
json=payload,
headers=HEADERS,
)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
if attempt < max_attempts:
print(f'Rate limited. Retrying in {retry_after}s...')
time.sleep(retry_after)
continue
if response.status_code == 409:
error_data = response.json()
if error_data.get('code') == 'CONCURRENCY_LIMIT_EXCEEDED':
if attempt < max_attempts:
print('Concurrency limit reached. Retrying in 10s...')
time.sleep(10)
continue
if response.status_code >= 500 and attempt < max_attempts:
backoff = min(2 ** attempt, 30)
print(f'Server error. Retrying in {backoff}s...')
time.sleep(backoff)
continue
response.raise_for_status()
return response.json()['jobId']
raise RuntimeError(f'Failed after {max_attempts} attempts')
Webhook handler with Flask
pip install flask
# webhook_server.py
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/viralsync', methods=['POST'])
def viralsync_webhook():
# Always respond 200 immediately
data = request.json
job_id = data['jobId']
status = data['status']
if status == 'done':
output_url = data['outputUrl']
print(f'Job {job_id} complete. URL: {output_url}')
# Download, store, notify user, etc.
elif status == 'failed':
error = data.get('error')
print(f'Job {job_id} failed: {error}')
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=5000)
Submit the job with callbackUrl: "https://your-server.com/webhooks/viralsync" to receive push notifications instead of polling.
Multi-scene video from a data list
Generate a product showcase video with one scene per product:
def build_product_showcase(products: list[dict]) -> dict:
"""
products: [{'name': 'Widget Pro', 'price': '$49', 'image_url': '...'}]
"""
scenes = []
for i, product in enumerate(products):
scene = {
'id': f'scene_{i}',
'duration': 5,
'bgColor': '#0f172a',
'layers': [
{
'id': f'img_{i}',
'type': 'image',
'source': product['image_url'],
'startTime': 0,
'duration': 5,
'zIndex': 0,
'position': {'x': 0, 'y': 0},
'size': {'width': 960, 'height': 1080},
},
{
'id': f'name_{i}',
'type': 'text',
'text': product['name'],
'startTime': 0,
'duration': 5,
'zIndex': 1,
'position': {'x': 980, 'y': 380},
'size': {'width': 880, 'height': 120},
'fontSize': 56,
'fontWeight': 'bold',
'color': '#ffffff',
'textAlign': 'left',
},
{
'id': f'price_{i}',
'type': 'text',
'text': product['price'],
'startTime': 0,
'duration': 5,
'zIndex': 1,
'position': {'x': 980, 'y': 520},
'size': {'width': 400, 'height': 80 },
'fontSize': 44,
'color': '#00d4ff',
'textAlign': 'left',
},
],
}
scenes.append(scene)
return {
'kind': 'movie',
'projectId': PROJECT_ID,
'renderOptions': {'qualityPreset': 'fast'},
'movie': {
'width': 1920,
'height': 1080,
'fps': 30,
'scenes': scenes,
},
}
# Usage
products = [
{'name': 'Widget Pro', 'price': '$49', 'image_url': 'https://cdn.example.com/widget.jpg'},
{'name': 'Gadget Plus', 'price': '$89', 'image_url': 'https://cdn.example.com/gadget.jpg'},
]
payload = build_product_showcase(products)
job_id = submit_render_with_retry(payload)
output_url = poll_until_done(job_id)
print(f'Showcase video: {output_url}')
Next steps
- Webhooks guide — replace polling with push
- Movie Schema — all layer properties
- n8n Automation — no-code automation workflows