Docs › Quickstart
Quickstart
Upload a video, wait for it to process, and download the MP4. Set two environment variables first:
export VIDEO_API="https://api.videopiper.dev/v1"
export VIDEO_API_KEY="vpk_..." # your key
# Uses jq to read JSON. Files up to 64 MiB are a single part (see Python/JavaScript for any size).
AUTH="Authorization: Bearer $VIDEO_API_KEY"; JSON="Content-Type: application/json"
# 1. Create the video
SIZE=$(wc -c < lesson.mp4 | tr -d ' ')
VIDEO_ID=$(curl -s -X POST "$VIDEO_API/videos" -H "$AUTH" -H "$JSON" \
-d "{\"fileName\":\"lesson.mp4\",\"sizeBytes\":$SIZE}" | jq -r .videoId)
# 2. Get an upload URL for part 1, and PUT the bytes to it (no Authorization header)
PART_URL=$(curl -s -X POST "$VIDEO_API/videos/$VIDEO_ID/parts" -H "$AUTH" -H "$JSON" \
-d '{"partNumbers":[1]}' | jq -r '.parts[0].url')
curl -s -T lesson.mp4 "$PART_URL"
# 3. Start processing
curl -s -X POST "$VIDEO_API/videos/$VIDEO_ID/complete" -H "$AUTH" -H "$JSON" -d '{}'
# 4. Wait for SUCCEEDED, then download
until [ "$(curl -s "$VIDEO_API/videos/$VIDEO_ID" -H "$AUTH" | jq -r .status)" != "RUNNING" ]; do sleep 10; done
curl -s "$VIDEO_API/videos/$VIDEO_ID" -H "$AUTH" | jq -r .status # expect SUCCEEDED
curl -s -o lesson-processed.mp4 "$(curl -s "$VIDEO_API/videos/$VIDEO_ID" -H "$AUTH" | jq -r .downloadUrl)"
# pip install requests. Handles any file size (parts are uploaded in turn).
import os, time, requests
API, KEY = os.environ["VIDEO_API"], os.environ["VIDEO_API_KEY"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {KEY}"
def call(method, path, **kwargs):
response = session.request(method, API + path, timeout=60, **kwargs)
if not response.ok:
error = response.json()
raise RuntimeError(f"{response.status_code} {error['error']}: {error['message']} (request {error['requestId']})")
return response.json()
path = "lesson.mp4"
size = os.path.getsize(path)
video = call("POST", "/videos", json={"fileName": os.path.basename(path), "sizeBytes": size, "maxHeight": 1080})
video_id, part_size, part_count = video["videoId"], video["partSize"], video["partCount"]
with open(path, "rb") as source:
for first in range(1, part_count + 1, 100): # up to 100 URLs per request
numbers = list(range(first, min(first + 100, part_count + 1)))
for part in call("POST", f"/videos/{video_id}/parts", json={"partNumbers": numbers})["parts"]:
source.seek((part["partNumber"] - 1) * part_size)
requests.put(part["url"], data=source.read(part["sizeBytes"]), timeout=600).raise_for_status()
call("POST", f"/videos/{video_id}/complete", json={})
while (status := call("GET", f"/videos/{video_id}"))["status"] in ("SUBMITTED", "RUNNING"):
time.sleep(10)
if status["status"] != "SUCCEEDED":
raise RuntimeError(f"Processing ended with {status['status']}")
with requests.get(status["downloadUrl"], stream=True, timeout=600) as download, open("lesson-processed.mp4", "wb") as out:
for chunk in download.iter_content(1 << 20):
out.write(chunk)
// Node.js 18+ (built-in fetch). Handles any file size (parts are uploaded in turn).
import { open, stat } from "node:fs/promises";
const API = process.env.VIDEO_API, KEY = process.env.VIDEO_API_KEY;
async function call(method, path, body) {
const response = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const data = await response.json();
if (!response.ok) throw new Error(`${response.status} ${data.error}: ${data.message} (request ${data.requestId})`);
return data;
}
const path = "lesson.mp4";
const { size } = await stat(path);
const { videoId, partSize, partCount } = await call("POST", "/videos", { fileName: "lesson.mp4", sizeBytes: size });
const file = await open(path);
for (let first = 1; first <= partCount; first += 100) { // up to 100 URLs per request
const partNumbers = Array.from({ length: Math.min(100, partCount - first + 1) }, (_, i) => first + i);
const { parts } = await call("POST", `/videos/${videoId}/parts`, { partNumbers });
for (const part of parts) {
const bytes = Buffer.alloc(part.sizeBytes);
await file.read(bytes, 0, part.sizeBytes, (part.partNumber - 1) * partSize);
const put = await fetch(part.url, { method: "PUT", body: bytes }); // no Authorization header
if (!put.ok) throw new Error(`Part ${part.partNumber} failed: ${put.status}`);
}
}
await file.close();
await call("POST", `/videos/${videoId}/complete`, {});
let status;
do {
await new Promise((r) => setTimeout(r, 10_000));
status = await call("GET", `/videos/${videoId}`);
} while (["SUBMITTED", "RUNNING"].includes(status.status));
console.log(status.status, status.downloadUrl);
Short videos are usually ready within a minute or two of complete; long videos are processed in parallel chunks.