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: Seedance 2.5 ($0.315/s, up to 30s), Seedance 2.0 ($0.227/s, up to 4K + RealFace), Seedance 2.0 Fast ($0.165/s, RealFace), Seedance 1.5 Pro ($0.070/s, image-to-video only), Sora 2 ($0.10/s, 4/8/12s only). Async flow: submit → poll → fetch the MP4 link.
Base URL: https://api.jarvisclaw.ai/v1
Authentication
Both methods are supported — all requests settle via x402 on-chain:
| Method | Header | Description |
|---|---|---|
| API Key | Authorization: Bearer sk-... | Platform signs x402 from your HD wallet automatically |
| Private Key (x402) | Automatic via SDK | Agent signs x402 directly from its own wallet |
See Agent Payments (x402) for full details on how both methods work.
Endpoints
POST /v1/videos/generations
Submit a video generation job.
| Name | Type | Required | Description |
|---|---|---|---|
| model | string | No | Video model ID (default: xai/grok-imagine-video). See Available Models below |
| prompt | string | Yes | Text description of the video to generate |
| duration_seconds | integer | No | Duration to bill for. Must respect the model's max: Seedance 2.5 up to 30s, Seedance 2.0/2.0-fast up to 15s, Seedance 1.5 Pro up to 12s, Sora discrete {4,8,12}s. Default: 5s (Seedance), 4s (Sora) |
| image_url | string | No | Seed image URL for image-to-video. For azure/sora-2 the image is resized server-side and must not contain a human face. Mutually exclusive with real_face_asset_id |
| real_face_asset_id | string | No | Character/face reference asset (ta_xxxxxx) from Virtual Portrait or RealFace. Seedance 2.0/2.0-fast only — not supported by Seedance 2.5. Mutually exclusive with image_url |
| resolution | string | No | Seedance only — defaults to 720p. Supported values differ per model: 480p / 720p for Seedance 2.5 and 2.0-fast, plus 1080p for 1.5 Pro, plus 1080p / 4K for 2.0. 360p, 540p, 1K and 2K are not supported by any model and are rejected with 400 before payment |
| aspect_ratio | string | No | adaptive / 16:9 / 9:16 / 1:1 / 4:3 / 3:4 / 21:9. Seedance only — ignored by Grok |
| generate_audio | boolean | No | Synced audio track. Seedance default: true for text-to-video, false for image/face-conditioned. Pass explicitly to override. Ignored by Grok |
| seed | integer | No | Reproducibility seed (Seedance). Same seed + prompt + params ≈ same clip |
| watermark | boolean | No | Embed upstream Seedance watermark. Off by default at the gateway |
| return_last_frame | boolean | No | Also return the last frame as a still — useful for chaining clips. Seedance only |
Request Examples
// Text-to-video
{
"model": "bytedance/seedance-2.0",
"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",
"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": "bytedance:video_ae260c45bdb7453c8bbe5a47",
"object": "video.generation.job",
"status": "queued",
"model": "bytedance/seedance-2.0",
"duration_seconds": 5,
"price": {
"amount": "1.135000",
"currency": "USD"
},
"payment_status": "verified",
"created": 1717200000,
"poll_url": "/v1/videos/generations/bytedance%3Avideo_ae260c45bdb7453c8bbe5a47?model=...&sig=..."
}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": "bytedance:video_ae260c45bdb7453c8bbe5a47",
"status": "in_progress",
"created_at": 1717200000,
"model": "bytedance/seedance-2.0",
"progress": 65,
"elapsed_seconds": 42
}Response (completed)
{
"id": "bytedance:video_ae260c45bdb7453c8bbe5a47",
"object": "video.generation.job",
"status": "completed",
"model": "bytedance/seedance-2.0",
"created": 1717200000,
"data": [
{
"url": "https://cdn.jarvisclaw.ai/media/media/videos/2026/06/20/video_ae260c45-15b82b3e.mp4",
"duration_seconds": 5,
"request_id": "video_ae260c45bdb7453c8bbe5a47"
}
],
"usage": {
"total_tokens": 108900
},
"price": {
"amount": "1.135000",
"currency": "USD"
}
}Response (failed)
{
"id": "bytedance:video_24d23caf344a431c90b43f64",
"status": "failed",
"error": "The request failed because the output audio may contain sensitive information.",
"message": "Generation was blocked by the provider's content filter. Try rephrasing the prompt.",
"retryable": false
}retryable tells you whether another attempt is worth making: transient upstream faults are retryable, content-filter rejections are not. Treat failed as terminal and stop polling.
Poll until terminal, not until timeout
Poll every 5–10s and stop on completed or failed. Content-filter false positives are the most common failure — an innocuous prompt can trip the audio filter, in which case rephrasing usually clears it.
Pricing
Video is billed per second of output at 720p, then scaled by a resolution multiplier. Multiply the per-second rate by your requested duration to get the clip cost.
Base rate (720p)
| Model | Price/second | 5s clip | Notes |
|---|---|---|---|
bytedance/seedance-2.5 | $0.315 | ~$1.58 | Newest generation, up to 30s, no RealFace |
bytedance/seedance-2.0 | $0.227 | ~$1.14 | Highest resolution ceiling (4K), supports RealFace |
bytedance/seedance-2.0-fast | $0.165 | ~$0.83 | 60–80s generation, supports RealFace |
bytedance/seedance-1.5-pro | $0.070 | ~$0.35 | Image-to-video only, no RealFace support |
azure/sora-2 | $0.10 | ~$0.50 (4s) | 720p + synced audio, 4/8/12s durations only |
xai/grok-imagine-video | $0.05 | ~$0.25 | Fast, lower fidelity |
| Virtual Portrait enrollment | — | $0.01 one-time | See RealFace |
| RealFace enrollment | — | $0.01 one-time | See RealFace |
Image-to-video is priced the same as text-to-video on every Seedance model.
Resolution multiplier (Seedance)
Higher resolutions consume more tokens upstream. The multiplier scales roughly with pixel count relative to 720p, and applies on top of each model's own per-second rate above. Only bytedance/seedance-2.0 reaches 4K; 1.5 Pro tops out at 1080p; Seedance 2.5 and 2.0-fast are 480p/720p only.
| Resolution | Multiplier | 5s (2.0) | 10s (2.0) | 15s (2.0) |
|---|---|---|---|---|
| 480p | ~0.44× | ~$0.50 | ~$1.00 | ~$1.50 |
| 720p | 1.0× | ~$1.14 | ~$2.27 | ~$3.41 |
| 1080p | ~2.25× | ~$2.56 | ~$5.11 | ~$7.67 |
| 4K | ~9.6× | ~$10.90 | ~$21.79 | ~$32.69 |
TIP
A 4K 15-second render on Seedance 2.0 runs to roughly $33. Use 720p for drafts and iterate before committing to high-resolution output.
For other models, take their per-second rate from the base table and apply the same multiplier — the ratios are not derived from the 2.0 rate.
Published price vs. settled amount
The rates above are reference rates. Upstream meters Seedance by token count, so the settled amount is priced for the exact duration and resolution you asked for. Always read price.amount in the response for the real figure.
Polling the job status is free — the GET reuses the original payment.
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", "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", "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 — job IDs are provider-prefixed and must be URL-encoded
curl "https://api.jarvisclaw.ai/v1/videos/generations/bytedance%3Avideo_ae260c45bdb7453c8bbe5a47" \
-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", 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",
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", 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/v2"
)
func main() {
ctx := context.Background()
vc, _ := jc.NewVideoClient(jc.WithAPIKey("sk-your-api-key"))
// Text-to-video (blocking — Generate waits by default)
job, _ := vc.Generate(ctx, "A cat playing piano in a jazz bar",
jc.WithVideoModel("bytedance/seedance-2.0"), jc.WithDuration(5))
fmt.Printf("Video URL: %s\n", job.URL)
// Non-blocking mode
job, _ = vc.Generate(ctx, "Ocean waves at sunset",
jc.WithVideoModel("bytedance/seedance-2.0-fast"),
jc.WithDuration(5), jc.WithWait(false))
fmt.Printf("Job ID: %s\n", job.ID)
// Poll manually
for job.Status != "completed" {
time.Sleep(10 * time.Second)
job, _ = vc.Status(ctx, job.ID)
}
fmt.Printf("Video URL: %s\n", job.URL)
// For image-to-video and RealFace, use cURL or the Python SDK —
// the Go VideoClient.Generate() does not yet accept image_url
// or real_face_asset_id parameters.
}package main
import (
"context"
"fmt"
"time"
jc "github.com/api-jarvisclaw/go-sdk/v2"
)
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 (blocking — Generate waits by default)
job, _ := vc.Generate(ctx, "A cat playing piano",
jc.WithVideoModel("bytedance/seedance-2.0"), jc.WithDuration(5))
fmt.Printf("Video URL: %s\n", job.URL)
// Non-blocking mode
job, _ = vc.Generate(ctx, "Ocean waves at sunset",
jc.WithVideoModel("bytedance/seedance-2.0-fast"),
jc.WithDuration(5), jc.WithWait(false))
fmt.Printf("Job ID: %s\n", job.ID)
// Poll manually
for job.Status != "completed" {
time.Sleep(10 * time.Second)
job, _ = vc.Status(ctx, job.ID)
}
fmt.Printf("Video URL: %s\n", job.URL)
// For image-to-video and RealFace, use cURL or the Python SDK —
// the Go VideoClient.Generate() does not yet accept image_url
// or real_face_asset_id parameters.
}Errors
| Code | Name | Description | Resolution |
|---|---|---|---|
| 400 | invalid_model | Requested video model not available | Use one of: bytedance/seedance-2.5, bytedance/seedance-2.0, 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 or 2.0 Fast 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 and Seedance 2.5 do NOT support real_face_asset_id — use 2.0 Fast or 2.0 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 2.5 up to 30s, Seedance 2.0/2.0-fast up to 15s, Seedance 1.5 Pro up to 12s, Sora 4/8/12s per clip
- Output resolution defaults to 720p. Ceilings differ per model: 4K on Seedance 2.0, 1080p on 1.5 Pro, 720p on Seedance 2.5 and 2.0-fast.
360p,540p,1Kand2Kare not supported by any model - 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
4K Resolution Example
Generate 4K resolution video using bytedance/seedance-2.0. Pass resolution: "4K" in the request body. Note that 4K output costs significantly more than the default 720p (see Pricing).
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", "prompt": "", "duration_seconds": 5, "resolution": "4K"}'import os, time, requests
API_KEY = os.environ["JARVISCLAW_API_KEY"]
BASE_URL = "https://api.jarvisclaw.ai/v1"
resp = requests.post(f"{BASE_URL}/videos/generations",
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json={"model": "bytedance/seedance-2.0", "prompt": "", "duration_seconds": 5, "resolution": "4K"},
timeout=60)
data = resp.json()
job_id = data["id"]
poll_url = f"{BASE_URL}/videos/generations/{requests.utils.quote(job_id, safe='')}"
# Poll until completed (max 10 min)
while True:
time.sleep(10)
poll = requests.get(poll_url, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=30).json()
if poll["status"] == "completed":
print(f"Video URL: {poll['data'][0]['url']}")
break
elif poll["status"] == "failed":
print(f"Failed: {poll}")
breakThe snippet above is the complete flow — submit, poll, download. Set
JARVISCLAW_API_KEYbefore running it.
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", "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",
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}")