Video Generation API
Generate AI videos from text prompts or images. Three input modes: text-to-video, image-to-video (animate a photo), and character-consistent video (via RealFace/Virtual Portrait asset_id). Models: Sora 2 (4/8/12s, $0.10/s), Seedance 1.5 Pro ($0.46/5s, image-to-video only), Seedance 2.0 Fast ($1.19/5s, supports RealFace), Seedance 2.0 Pro ($1.49/5s, highest quality). Async workflow: submit → poll → get MP4 URL.
Base URL: https://api.jarvisclaw.ai/v1
Endpoints
POST /v1/videos/generations
Submit a video generation job.
| Name | Type | Required | Description |
|---|---|---|---|
| model | string | Yes | Model ID: "bytedance/seedance-2.0-pro", "bytedance/seedance-2.0-fast", "bytedance/seedance-1.5-pro", "azure/sora-2" |
| prompt | string | Yes | Text description of the video to generate |
| duration_seconds | integer | No | Duration (Seedance: 5-10s, Sora: 4/8/12s) (default: 5) |
| image_url | string | No | Public HTTPS URL of seed image for image-to-video (animates the photo) |
| real_face_asset_id | string | No | Enrolled RealFace/Portrait asset ID (ta_xxx) for character-consistent generation. Only works with Seedance 2.0/2.0-fast. |
Request Examples
// Text-to-video
{
"model": "bytedance/seedance-2.0-pro",
"prompt": "A cat playing piano in a dimly lit jazz bar",
"duration_seconds": 5
}
// Image-to-video (animate a photo)
{
"model": "bytedance/seedance-2.0-pro",
"prompt": "Person waving hello and smiling",
"image_url": "https://example.com/person-photo.jpg",
"duration_seconds": 5
}
// With RealFace character (enrolled person)
{
"model": "bytedance/seedance-2.0-fast",
"prompt": "Person giving a presentation in a modern office",
"real_face_asset_id": "ta_f85b20b9394e47be9502d819bee7929c",
"duration_seconds": 5
}Response
{
"id": "vg_abc123def456",
"status": "queued",
"created_at": 1717200000,
"model": "bytedance/seedance-2.0-pro",
"estimated_wait_seconds": 60
}GET /v1/videos/generations/:id
Poll video generation job status. Call every 5-10s until status is "completed" or "failed". Jobs remain retrievable for 48 hours after submission.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Job ID returned from the generation request |
Response (in progress)
{
"id": "vg_abc123def456",
"status": "in_progress",
"created_at": 1717200000,
"model": "bytedance/seedance-2.0-pro",
"progress": 65,
"elapsed_seconds": 42
}Response (completed)
{
"id": "vg_abc123def456",
"status": "completed",
"created_at": 1717200000,
"completed_at": 1717200065,
"model": "bytedance/seedance-2.0-pro",
"url": "https://cdn.jarvisclaw.ai/videos/vg_abc123def456.mp4",
"duration_seconds": 5,
"progress": 100,
"elapsed_seconds": 65
}Pricing
| Model | Price | Notes |
|---|---|---|
| azure/sora-2 | $0.10/sec | 720p + synced audio, 4/8/12s clips |
| bytedance/seedance-1.5-pro | $0.46/5s | Image-to-video only, no RealFace support |
| bytedance/seedance-2.0-fast | $1.19/5s | 60-80s generation, supports RealFace |
| bytedance/seedance-2.0-pro | $1.49/5s | Highest quality, supports RealFace |
| Virtual Portrait enrollment | $0.01 (one-time) | See /docs/api/realface |
| RealFace enrollment | $0.01 (one-time) | See /docs/api/realface |
Code Examples
# Text-to-video
curl -X POST https://api.jarvisclaw.ai/v1/videos/generations \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"model": "bytedance/seedance-2.0-pro", "prompt": "A cat playing piano in a jazz bar", "duration_seconds": 5}'
# Image-to-video (animate a photo)
curl -X POST https://api.jarvisclaw.ai/v1/videos/generations \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"model": "bytedance/seedance-2.0-pro", "prompt": "Person walks forward", "image_url": "https://example.com/photo.jpg", "duration_seconds": 5}'
# With RealFace character
curl -X POST https://api.jarvisclaw.ai/v1/videos/generations \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"model": "bytedance/seedance-2.0-fast", "prompt": "Person presenting", "real_face_asset_id": "ta_xxx", "duration_seconds": 5}'
# Poll status
curl https://api.jarvisclaw.ai/v1/videos/generations/vg_abc123 \
-H "Authorization: Bearer sk-your-api-key"from jarvisclaw import VideoClient
video = VideoClient(api_key="sk-your-api-key")
# ─── Blocking mode (SDK auto-polls, default 10min timeout) ───
job = video.generate("A cat playing piano in a jazz bar",
model="bytedance/seedance-2.0-pro", duration=5)
print(f"Video URL: {job.url}")
# Image-to-video (animate a photo)
job = video.generate("Person waving hello",
model="bytedance/seedance-2.0-pro",
image_url="https://example.com/photo.jpg", duration=5)
print(f"Video URL: {job.url}")
# ─── Non-blocking mode (fire and forget) ───
job = video.generate("Ocean waves at sunset", wait=False)
print(f"Submitted: {job.id}")
# ... later (within 48 hours) ...
result = video.wait(job.id)
print(f"Video URL: {result.url}")
# ─── Timeout-safe pattern ───
job = video.generate("Flower blooming timelapse", poll_timeout=180)
if job.status == "timeout":
# NOT lost — just retry later
print(f"Progress: {job.raw.get('progress')}%")
result = video.wait(job.id) # continue polling
print(result.url)from jarvisclaw import VideoClient
# ─── Base chain (EVM) — USDC on Base ───
video = VideoClient(private_key="0x<evm-private-key>")
# ─── Solana — USDC SPL on Solana mainnet ───
# video = VideoClient(private_key="<solana-bs58-keypair>")
# SDK auto-detects chain from key format
# Blocking (auto-polls until done, no charge until "completed")
job = video.generate("A cat playing piano in a jazz bar",
model="bytedance/seedance-2.0-pro", duration=5)
print(f"Video URL: {job.url}")
# Non-blocking (submit and retrieve later)
job = video.generate("Ocean waves at sunset", wait=False)
print(f"Job ID: {job.id}") # Save this
# Retrieve anytime within 48 hours
result = video.wait(job.id)
print(f"Video URL: {result.url}")
# Timeout-safe: progress is preserved
job = video.generate("Flower timelapse", poll_timeout=180)
if job.status == "timeout":
print(f"Progress: {job.raw.get('progress')}%")
result = video.wait(job.id)
print(result.url)package main
import (
"context"
"fmt"
"time"
jc "github.com/api-jarvisclaw/go-sdk"
)
func main() {
ctx := context.Background()
vc, _ := jc.NewVideoClient(jc.WithAPIKey("sk-your-api-key"))
// Text-to-video
job, _ := vc.Generate(ctx, "A cat playing piano in a jazz bar",
jc.WithModel("bytedance/seedance-2.0-pro"), jc.WithDuration(5))
// Image-to-video
job, _ = vc.Generate(ctx, "Person waving",
jc.WithModel("bytedance/seedance-2.0-pro"),
jc.WithImageURL("https://example.com/photo.jpg"),
jc.WithDuration(5))
// With RealFace character
job, _ = vc.Generate(ctx, "Person presenting",
jc.WithModel("bytedance/seedance-2.0-fast"),
jc.WithRealFaceAssetID("ta_xxx"),
jc.WithDuration(5))
// Poll until complete
for job.Status != "completed" {
time.Sleep(10 * time.Second)
job, _ = vc.Status(ctx, job.ID)
}
fmt.Printf("Video URL: %s\n", job.URL)
}package main
import (
"context"
"fmt"
"time"
jc "github.com/api-jarvisclaw/go-sdk"
)
func main() {
ctx := context.Background()
// x402 Agent wallet — pays per-call via USDC on Base (Chain ID 8453)
vc, _ := jc.NewVideoClient(jc.WithPrivateKey("0x<evm-private-key>"))
// Text-to-video
job, _ := vc.Generate(ctx, "A cat playing piano",
jc.WithModel("bytedance/seedance-2.0-pro"), jc.WithDuration(5))
// Image-to-video
job, _ = vc.Generate(ctx, "Person waving",
jc.WithModel("bytedance/seedance-2.0-pro"),
jc.WithImageURL("https://example.com/photo.jpg"),
jc.WithDuration(5))
// With RealFace character
job, _ = vc.Generate(ctx, "Person presenting",
jc.WithModel("bytedance/seedance-2.0-fast"),
jc.WithRealFaceAssetID("ta_xxx"),
jc.WithDuration(5))
// Poll until complete
for job.Status != "completed" {
time.Sleep(10 * time.Second)
job, _ = vc.Status(ctx, job.ID)
}
fmt.Printf("Video URL: %s\n", job.URL)
}Errors
| Code | Name | Description | Resolution |
|---|---|---|---|
| 400 | invalid_model | Requested video model not available | Use one of: bytedance/seedance-2.0-pro, bytedance/seedance-2.0-fast, bytedance/seedance-1.5-pro, azure/sora-2 |
| 400 | invalid_asset_id | real_face_asset_id format invalid or not found | Enroll via /v1/marketplace/realface/enroll first to get a valid ta_xxx ID |
| 400 | model_asset_incompatible | real_face_asset_id used with Seedance 1.5 Pro (unsupported) | Use Seedance 2.0 Fast or 2.0 Pro for RealFace generation |
| 408 | generation_timeout | Video generation exceeded maximum time (5 minutes) | Retry the request — upstream may be under heavy load |
| 404 | job_not_found | Job ID does not exist or has expired (48h) | Job results are available for 48 hours after submission. After that, re-submit. |
Limitations
- Seedance 1.5 Pro does NOT support real_face_asset_id — use 2.0 Fast or 2.0 Pro for character-consistent video
- image_url and real_face_asset_id are mutually exclusive — use one or the other, not both
- Generation takes 60-180 seconds — use async polling, not synchronous waiting
- Maximum duration: Seedance 5-10s per clip, Sora 4/8/12s per clip
- Output resolution fixed at 720p (1280x720) — no 1080p or 4K
- RealFace enrollment required separately via /docs/api/realface before using real_face_asset_id
- Job results (MP4 URLs) are retrievable for 48 hours after submission — even if your client disconnects
- Status sync lag: upstream may report "in_progress" for a few minutes after actual completion — keep polling, do not cancel early
Guides
Async Polling & Timeout Handling
Video generation is asynchronous — you submit a job, then poll until it completes. Jobs are retained for 48 hours, so even if your client times out, you can always come back and retrieve the result later.
Submit and poll (basic pattern)
Submit the generation request, get a job ID, poll every 5-10 seconds until status is "completed". The response includes progress (0-100) and elapsed_seconds to monitor generation.
# ── Python (recommended: SDK handles all of this) ──
from jarvisclaw import VideoClient
video = VideoClient(api_key="sk-your-key")
# Blocking mode — SDK auto-polls until done (up to 10 min)
job = video.generate("A sunset over the ocean", duration=5)
print(job.url) # MP4 URL# ── cURL (manual polling) ──
# Step 1: Submit
curl -X POST https://api.jarvisclaw.ai/v1/videos/generations \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{"model": "bytedance/seedance-2.0-fast", "prompt": "A sunset over the ocean", "duration_seconds": 5}'
# → {"id": "vg_abc123", "status": "queued"}
# Step 2: Poll every 5-10s
curl https://api.jarvisclaw.ai/v1/videos/generations/vg_abc123 \
-H "Authorization: Bearer sk-your-key"
# → {"id": "vg_abc123", "status": "in_progress", "progress": 65, "elapsed_seconds": 42}
# ... keep polling ...
# → {"id": "vg_abc123", "status": "completed", "url": "https://...mp4"}Non-blocking mode (fire and forget)
Submit the job without waiting. Save the job ID, do other work, then check back later.
from jarvisclaw import VideoClient
video = VideoClient(api_key="sk-your-key")
# Submit without waiting
job = video.generate("Cat on the moon", wait=False)
print(f"Job submitted: {job.id}") # Save this ID
# ... minutes or hours later ...
result = video.wait(job.id) # Blocks until ready
print(result.url)
# Or just check once (non-blocking)
result = video.status(job.id)
if result.status == "completed":
print(result.url)
else:
print(f"Still working: {result.raw.get('progress', '?')}%")Timeout handling (status sync lag)
Upstream providers may report "in_progress" for several minutes after actual completion (status sync lag). If your poll times out, DO NOT re-submit — the job is still valid.
from jarvisclaw import VideoClient
video = VideoClient(api_key="sk-your-key")
# Try with a shorter timeout for fast feedback
job = video.generate("Timelapse of flowers blooming",
poll_timeout=180) # 3 min timeout
if job.status == "timeout":
# Job is NOT lost! Just poll again later
print(f"Still generating, job ID: {job.id}")
print(f"Progress: {job.raw.get('progress', 'unknown')}%")
print(f"Elapsed: {job.raw.get('elapsed_seconds', '?')}s")
# Come back anytime within 48h:
result = video.wait(job.id, poll_timeout=600)
print(result.url)
# ── Important notes ──
# 1. Never re-submit on timeout — you'll pay twice
# 2. Jobs survive client disconnects — 48h retention
# 3. Billing only occurs when you successfully poll "completed"
# 4. If progress stays at 0% for 5+ minutes, contact supportRealFace Video (Real Person)
Generate videos featuring a real person with consistent identity. The person must complete a one-time liveness check (~60 seconds) to verify their identity.
See RealFace API for full enrollment documentation.
Virtual Portrait (AI Character)
Create a consistent AI character identity without any real person. Upload any character image (illustration, 3D render, AI face) and reuse it across videos.
See RealFace API for full enrollment documentation.
Image-to-Video (Animate a Photo)
Take any static image (photo, illustration, screenshot) and animate it into a video clip.
curl -X POST https://api.jarvisclaw.ai/v1/videos/generations \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"model": "bytedance/seedance-2.0-pro", "prompt": "The person turns and smiles at the camera", "image_url": "https://example.com/portrait.jpg", "duration_seconds": 5}'from jarvisclaw import VideoClient
video = VideoClient(private_key="0x<evm-private-key>")
job = video.generate("The person turns and smiles at the camera",
model="bytedance/seedance-2.0-pro",
image_url="https://example.com/portrait.jpg",
duration=5)
print(f"Job submitted: {job.id}")
import time
while job.status != "completed":
time.sleep(10)
job = video.status(job.id)
print(f"Video URL: {job.url}")