Generate personalized patient education videos programmatically. One API call, ~60 seconds, HD video with voiceover.
Generate a video in 3 steps:
1. Start generation
curl -X POST /api/v1/video/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"patient_name": "Sarah Johnson",
"doctor_name": "Dr. Martinez",
"clinic_name": "Bright Smiles Dental",
"treatment": "crown",
"treatment_notes": "MOD composite #14",
"video_goal": "educate",
"patient_status": "undecided",
"concerns": "worried about pain"
}'2. Poll status (every 3-5 seconds)
curl /api/v1/video/JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"3. Download when ready
# video_url from the status response is a direct download link
curl -L /api/v1/video/JOB_ID/download \
-H "Authorization: Bearer YOUR_API_KEY" \
-o video.mp4All endpoints require a Bearer token in the Authorization header.
Authorization: Bearer opk_a1b2c3d4e5f6...Contact Opera to get your API key.
POST /api/v1/video/generate| Parameter | Type | Description | |
|---|---|---|---|
| patient_name | string | required | Patient's full name. |
| doctor_name | string | required | Doctor's name (include 'Dr.' prefix). |
| clinic_name | string | required | Clinic or practice name. |
| treatment | string | required | Treatment type. See Treatments section. |
| treatment_notes | string | optional | Clinical notes (e.g. 'MOD composite #14'). |
| video_goal | string | optional | Video purpose. Default: "educate" |
| patient_status | string | optional | Where the patient is in their journey. Default: "undecided" |
| appointment_context | string | optional | Type of appointment. Default: "new_patient_consult" |
| content_mode | string | optional | Script generation mode. Default: "template" |
| concerns | string | optional | Patient concerns (e.g. 'afraid of needles'). |
| financing | string | optional | Financing info (e.g. 'accepts CareCredit'). |
| urgency_level | string | optional | Urgency: routine, moderate, urgent. Default: "routine" |
| parent_mode | boolean | optional | Video is for a child's parent. Default: false |
| brand_primary_color | string | optional | Hex color for clinic branding. Default: "#7c3aed" |
{
"id": "uuid-job-id",
"status": "processing",
"poll_url": "/api/v1/video/uuid-job-id",
"created_at": "2026-01-15T10:30:00.000Z"
}GET /api/v1/video/:idPoll every 3-5 seconds until status is completed or failed.
// Processing
{ "id": "...", "status": "processing", "progress": 0.65, "step": "Rendering video" }
// Completed
{ "id": "...", "status": "completed", "progress": 1.0,
"video_url": "https://...s3.amazonaws.com/videos/...",
"download_url": "/api/v1/video/.../download" }
// Failed
{ "id": "...", "status": "failed", "error": "Description of error" }GET /api/v1/video/:id/downloadRedirects (302) to the video file. The video_url from the status endpoint is also a direct link.
Diagnosis and specialty are auto-derived from the treatment type.
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_json | Request body is not valid JSON. |
| 401 | unauthorized | Missing or invalid API key. |
| 404 | not_found | Job ID does not exist. |
| 422 | validation_error | Input validation failed. Check details array. |
| 429 | rate_limited | Rate limit exceeded. Check Retry-After header. |
| 500 | internal_error | Unexpected server error. |
Default limits per API key:
Contact Opera for higher limits. When rate limited, the response includes a Retry-After header (seconds).
import requests, time
API_KEY = "opk_your_key_here"
BASE = ""
# 1. Generate
resp = requests.post(f"{BASE}/api/v1/video/generate",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"patient_name": "Sarah Johnson",
"doctor_name": "Dr. Martinez",
"clinic_name": "Bright Smiles Dental",
"treatment": "crown"
})
job = resp.json()
job_id = job["id"]
# 2. Poll
while True:
status = requests.get(f"{BASE}/api/v1/video/{job_id}",
headers={"Authorization": f"Bearer {API_KEY}"}).json()
if status["status"] == "completed":
print("Video URL:", status["video_url"])
break
elif status["status"] == "failed":
print("Error:", status["error"])
break
time.sleep(3)const API_KEY = "opk_your_key_here";
const BASE = "";
// 1. Generate
const { id } = await fetch(`${BASE}/api/v1/video/generate`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
patient_name: "Sarah Johnson",
doctor_name: "Dr. Martinez",
clinic_name: "Bright Smiles Dental",
treatment: "crown"
})
}).then(r => r.json());
// 2. Poll
while (true) {
const status = await fetch(`${BASE}/api/v1/video/${id}`, {
headers: { "Authorization": `Bearer ${API_KEY}` }
}).then(r => r.json());
if (status.status === "completed") {
console.log("Video:", status.video_url);
break;
}
if (status.status === "failed") throw new Error(status.error);
await new Promise(r => setTimeout(r, 3000));
}