O

Opera Video API

Generate personalized patient education videos programmatically. One API call, ~60 seconds, HD video with voiceover.

Quick Start

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.mp4

Authentication

All endpoints require a Bearer token in the Authorization header.

Authorization: Bearer opk_a1b2c3d4e5f6...

Contact Opera to get your API key.

Generate Video

POST /api/v1/video/generate
ParameterTypeDescription
patient_namestringrequiredPatient's full name.
doctor_namestringrequiredDoctor's name (include 'Dr.' prefix).
clinic_namestringrequiredClinic or practice name.
treatmentstringrequiredTreatment type. See Treatments section.
treatment_notesstringoptionalClinical notes (e.g. 'MOD composite #14').
video_goalstringoptionalVideo purpose. Default: "educate"
patient_statusstringoptionalWhere the patient is in their journey. Default: "undecided"
appointment_contextstringoptionalType of appointment. Default: "new_patient_consult"
content_modestringoptionalScript generation mode. Default: "template"
concernsstringoptionalPatient concerns (e.g. 'afraid of needles').
financingstringoptionalFinancing info (e.g. 'accepts CareCredit').
urgency_levelstringoptionalUrgency: routine, moderate, urgent. Default: "routine"
parent_modebooleanoptionalVideo is for a child's parent. Default: false
brand_primary_colorstringoptionalHex color for clinic branding. Default: "#7c3aed"

Response (202 Accepted)

{
  "id": "uuid-job-id",
  "status": "processing",
  "poll_url": "/api/v1/video/uuid-job-id",
  "created_at": "2026-01-15T10:30:00.000Z"
}

Check Status

GET /api/v1/video/:id

Poll 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" }

Download Video

GET /api/v1/video/:id/download

Redirects (302) to the video file. The video_url from the status endpoint is also a direct link.

Supported Treatments

crown
filling
root_canal
implant
extraction
bridge
veneers
whitening
gum_treatment
dentures
full_mouth_rehab
braces
invisalign
ceramic_braces
lingual_braces
expander
retainer
jaw_surgery

Diagnosis and specialty are auto-derived from the treatment type.

Error Codes

HTTPCodeDescription
400invalid_jsonRequest body is not valid JSON.
401unauthorizedMissing or invalid API key.
404not_foundJob ID does not exist.
422validation_errorInput validation failed. Check details array.
429rate_limitedRate limit exceeded. Check Retry-After header.
500internal_errorUnexpected server error.

Rate Limits

Default limits per API key:

  • 10 requests/minute
  • 500 requests/day

Contact Opera for higher limits. When rate limited, the response includes a Retry-After header (seconds).

Code Examples

Python

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)

Node.js

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));
}

Try It Live

Try It