Video Generation API
Generate short AI videos with OpenAI Sora 2, xAI Grok Imagine, or ByteDance Seedance. Text (or image) prompt in, MP4 URL out — paid per call with USDC on Base via x402.
- Endpoint:
POST https://blockrun.ai/v1/videos/generations - Poll:
GET https://blockrun.ai/v1/videos/generations/{id}?model=…&duration=… - Payment: x402, USDC on Base mainnet (
network: "base",x402Version: 2). Minimum charge $0.003.
The /v1/... and /api/v1/... paths are equivalent (the gateway rewrites /v1 → /api/v1). Examples below use /v1.
Sample output
A real azure/sora-2 clip generated through this exact API — 4s, 720p, synced audio (text-to-video):
<video src="https://blockrun.ai/api/media/media/videos/2026/05/27/video_6a1670cac7a081909309f1b6b85fbb40-6af6c7a4.mp4" controls muted width="480"></video>
▶️ Open the sample clip directly if the inline player doesn't load.
How it works — async submit → poll → settle
Video generation is asynchronous and two-step. A clip takes ~60–180s upstream, far longer than a single HTTP request should stay open, so the flow is:
POST /v1/videos/generations — verifies your x402 payment (verify only, no charge yet) and submits the upstream job. Returns 202 in ~3–20s with a job id and a poll_url.
GET {poll_url} — poll every 5–10s with an x-payment header signed by the same wallet. While the job runs you get 202. When it finishes you get 200 with the video URL, and that is the moment you are charged (settlement happens on the first completed poll).
Settlement only fires on a completed poll. If the upstream job fails, times out, or you never poll, no USDC moves. Re-polling an already-settled job returns the same video URL (payment.status: "already_settled") — you are never double-charged.
Key guarantees:
| Guarantee | Detail |
|---|---|
| No charge on failure | If the upstream job fails or you never poll, no USDC moves. Settlement only fires on a completed poll. |
| Wallet binding, not signature equality | The poll must be signed by the wallet that submitted the POST. A fresh signature from that same wallet is fine — the poll endpoint returns its own 402 challenge if no header is sent, so standard x402 clients re-sign automatically. |
| Idempotent re-polls | Polling an already-settled job returns the same video URL again (payment.status: "already_settled") — you are never double-charged. |
| Replay-protected | Each signed authorization can submit exactly one job (nonce claim on POST). |
| Durable output | The clip is mirrored to BlockRun's GCS bucket before settlement; data[].url is the permanent BlockRun-hosted URL, data[].source_url is the (often temporary) upstream URL. |
The TypeScript VideoClient and the local ClawRouter proxy run the submit+poll loop for you, so you make a single call and get the finished video back. The two-step contract below is only relevant if you call the raw HTTP API yourself.
Available models
| Model ID | Name | Durations (sec) | Resolution | Image-to-video | Synced audio | Character / RealFace asset |
|---|---|---|---|---|---|---|
azure/sora-2 | Sora 2 | 4 / 8 / 12 (default 4 — only these three) | 720p, portrait or landscape | ✅ (non-human only) | ✅ | ❌ |
xai/grok-imagine-video | Grok Imagine Video | 1–15 (default 8) | upstream default | ✅ | — | ❌ |
bytedance/seedance-1.5-pro | Seedance 1.5 Pro | default 5, max 12 | 720p (default) | ✅ | ✅ (t2v) | ❌ |
bytedance/seedance-2.0-fast | Seedance 2.0 Fast | default 5, max 15 | 720p (default) | ✅ | ✅ (t2v) | ✅ |
bytedance/seedance-2.0 (Pro) | Seedance 2.0 Pro | default 5, max 15 | 720p (default), up to 4K | ✅ | ✅ (t2v) | ✅ |
Notes:
- Sora 2 accepts only
duration_secondsof 4, 8, or 12 — any other value returns400listing the allowed set. Text-to-video only (noimage_url). Output is 720p with synchronized audio, portrait or landscape. - Grok Imagine accepts
duration_secondsfrom 1 to 15 (default 8); it accepts an optionalimage_urland ignores the Seedance-only tuning params (resolution,aspect_ratio,generate_audio, etc.). - Seedance supports a default of 5s and a per-tier maximum —
seedance-2.0/seedance-2.0-fastallow up to 15s,seedance-1.5-proup to 12s (the gateway returns400for aduration_secondsabove the tier max). The gateway bumps the default to 720p and setsgenerate_audioper the t2v/i2v split below. Only Seedance 2.0 / 2.0-fast accept areal_face_asset_id(ta_xxxx) for character/identity consistency, multiple reference images, and reference video/audio (r2v).seedance-2.0-fastfinishes in ~60–80s;seedance-2.0(Pro) is higher quality and slower.
Image-to-video options
Whether you can seed generation from an image — and how — depends on the subject:
- Non-human subject (product, scene, animal, object): pass
image_url(a public URL to the first frame) onazure/sora-2, Grok, or any Seedance model. Forazure/sora-2the gateway resizes the seed image server-side to Sora's exact required dimensions (1280×720 / 720×1280). Seedance image-to-video is billed at the same per-token rate as text-to-video. - A specific real person: you cannot upload a face to Sora (see the note below). Use Seedance 2.0 / 2.0-fast + a RealFace
ta_xxxxasset — enroll the person once with their consent (RealFace, ~1-min on-phone liveness, $0.012), then passreal_face_asset_id. Details in Character consistency below. - An AI character / mascot: same flow with a Virtual Portrait asset (no KYC, $0.012).
azure/sora-2 rejects reference images that contain human faces — a moderation pipeline blocks any recognizable person to prevent deepfakes, and there is no general human-likeness image-upload path. So on BlockRun: azure/sora-2 does image-to-video for non-human subjects (image_url, resized server-side to Sora's exact dimensions); and real-person video goes through Seedance 2.0 + RealFace (the consent-based route above).
Request parameters (POST body, JSON)
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | No | Video model ID (default xai/grok-imagine-video). See table above. |
prompt | string | Yes | Text description of the video to generate. |
image_url | string (URL) | No | Seed image for image-to-video (all video models support it). For azure/sora-2 the image is resized server-side to Sora's exact dimensions 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 a Virtual Portrait (AI character) or RealFace (real person). Seedance 2.0 / 2.0-fast only. Mutually exclusive with image_url. |
last_frame_url | string (URL) | No | Final-frame target for first-and-last-frame interpolation. Send with image_url (first frame). Seedance only (1.5-pro, 2.0-fast, 2.0). |
reference_image_urls | string[] | No | Multiple reference images for character/style consistency (cited as "image 1", "image 2"). seedance-2.0 / seedance-2.0-fast only. |
reference_videos | array of {url} | No | Reference videos for reference-to-video (r2v). Up to 3. seedance-2.0 / seedance-2.0-fast only. Adds a token surcharge (see Pricing). Flat video_url is rejected — use this array. |
reference_audios | array of {url} | No | Reference audios for r2v. Up to 3, each ≤15.2s. seedance-2.0 / seedance-2.0-fast only. Adds a token surcharge. Flat audio_url is rejected — use this array. |
input_type | string | No | Optional validated enum: text / image / first_last_frame / reference. When supplied it must match the seed fields you actually sent or the request returns a 400. |
duration_seconds | integer | No | Duration to bill for. Defaults to the model default. Must respect the model's max (Seedance: 2.0 / 2.0-fast = 15s, 1.5-pro = 12s; Sora 2: the discrete {4,8,12} set) or you get a 400. |
resolution | string | No | 360p / 480p / 540p / 720p / 1080p / 1K, plus 4K on bytedance/seedance-2.0 only (real 3840×2160). Seedance defaults to 720p; higher resolutions cost more tokens upstream. On seedance-1.5-pro / seedance-2.0-fast the max is 1080p/1K, and 4K returns a 400. 2K is never offered (not produced upstream). Seedance only. |
aspect_ratio | string | No | adaptive / 16:9 / 9:16 / 1:1 / 4:3 / 3:4 / 21:9 / 9:21. 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 the 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. |
Pricing
All prices include the gateway's standard 5% margin — i.e. these are the amounts quoted in the 402 challenge and actually billed in USDC.
| Model | Billing basis | Effective price (720p) |
|---|---|---|
azure/sora-2 | $0.10 / second (flat) | 4s = $0.42 · 8s = $0.84 · 12s = $1.26 |
xai/grok-imagine-video | $0.05 / second (flat) | 8s = $0.42 · 15s = $0.79 |
bytedance/seedance-1.5-pro | Token-metered ($4.32 / M tokens) | 5s ≈ $0.49 · 12s ≈ $1.18 |
bytedance/seedance-2.0-fast | Token-metered ($11.20 / M tokens) | 5s ≈ $1.28 · 15s ≈ $3.82 |
bytedance/seedance-2.0 (Pro) | Token-metered ($14 / M tokens) | 5s ≈ $1.59 · 15s ≈ $4.78 |
Seedance token math: at the 720p default a clip uses ~21,690 tokens/second (a 5s clip ≈ 108,450 tokens). Price = duration × 21,690 × resolution-factor × rate-per-M ÷ 1,000,000 × 1.05. Image-to-video is billed at the same per-token rate as text-to-video (no i2v discount). The resolution token factor relative to 720p (=1) is: 360p ×0.3, 480p ×0.5, 540p ×0.7, 720p ×1, 1080p/1K ×2.25, and 4K ×9 (area-proportional) — so dropping to 480p halves the per-clip cost and 1080p/4K cost proportionally more. 4K (real 3840×2160) is available only on bytedance/seedance-2.0; on seedance-1.5-pro/seedance-2.0-fast the ceiling is 1080p/1K and 4K returns a 400. 2K is never offered (not produced upstream). Reference video/audio inputs (r2v) add a surcharge: ×(1 + 1.0·#refVideos + 0.3·#refAudios). Per-second models (Grok, Sora) ignore resolution and reference surcharges.
One-time enrollment fees (separate from per-call billing):
| Action | Endpoint | Price |
|---|---|---|
| Virtual Portrait enrollment | POST /v1/portrait/enroll | $0.012 USDC per asset (no KYC) |
| RealFace enrollment | POST /v1/realface/enroll | $0.012 USDC per asset (no KYC, requires ~1-min on-phone liveness) |
Responses
1. POST → 202 Accepted (job submitted)
{
"id": "azure:vidjob_abc123",
"object": "video.generation.job",
"status": "queued",
"model": "azure/sora-2",
"duration_seconds": 8,
"price": { "amount": "0.840000", "currency": "USD" },
"payment_status": "verified",
"created": 1776443975,
"poll_url": "/api/v1/videos/generations/azure%3Avidjob_abc123?model=azure%2Fsora-2&duration=8"
}
The id is a composite "{provider}:{upstreamId}". The poll_url already encodes the required ?model and ?duration query params — preserve them when polling.
2. GET poll → 202 (still generating)
{
"id": "azure:vidjob_abc123",
"object": "video.generation.job",
"status": "in_progress",
"model": "azure/sora-2",
"payment_status": "verified",
"note": "Upstream is still generating. Poll again in 5-10s. No charge until status=completed."
}
status is queued or in_progress. Keep polling every 5–10s.
3. GET poll → 200 (completed — charged here)
{
"id": "azure:vidjob_abc123",
"object": "video.generation.job",
"status": "completed",
"model": "azure/sora-2",
"created": 1776444180,
"data": [
{
"url": "https://blockrun.ai/api/media/media/videos/2026/05/27/<id>.mp4",
"source_url": "https://<upstream-host>/<id>.mp4",
"duration_seconds": 8,
"request_id": "vidjob_abc123",
"backed_up": true
}
],
"payment": { "status": "settled", "tx_hash": "0x…", "network": "base" }
}
On settlement the response also carries PAYMENT-RESPONSE and X-Payment-Receipt (the on-chain tx hash) headers. A re-poll of an already-settled job returns the same body with payment.status: "already_settled" and no new charge.
GET poll → 200 (failed — not charged)
{
"id": "azure:vidjob_abc123",
"object": "video.generation.job",
"status": "failed",
"model": "azure/sora-2",
"error": "<upstream reason>",
"payment_status": "not_charged",
"note": "Upstream generation failed. No payment was taken."
}
Response fields
| Field | Type | Description |
|---|---|---|
id | string | Composite job id "{provider}:{upstreamId}" |
status | string | queued → in_progress → completed | failed |
data[].url | string | Permanent BlockRun-hosted URL (GCS-backed). Falls back to upstream URL if backup fails. |
data[].source_url | string | Original upstream URL (may expire) |
data[].duration_seconds | integer | Duration of the generated clip |
data[].request_id | string | Upstream request id for debugging |
data[].backed_up | boolean | true when mirrored to BlockRun's GCS bucket |
payment.status | string | settled | already_settled |
payment.tx_hash | string | On-chain USDC settlement tx (also in X-Payment-Receipt header) |
usage | object | On a completed Seedance poll: a usage block with total_tokens (the metered token count the price was computed from). |
Examples
Image-to-video (Grok / Seedance)
curl -X POST https://blockrun.ai/v1/videos/generations \
-H "Content-Type: application/json" \
-H "X-Payment: $PAYMENT_HEADER" \
-d '{ "model": "bytedance/seedance-2.0", "prompt": "the subject turns and smiles", "image_url": "https://example.com/portrait.jpg" }'
Character consistency (Seedance 2.0 fast / pro)
Pass a ta_xxxx asset from a Virtual Portrait or RealFace enrollment to keep the same identity across clips. Mutually exclusive with image_url.
{
"model": "bytedance/seedance-2.0",
"prompt": "the subject smiles warmly and waves at the camera",
"real_face_asset_id": "ta_abc123xyz"
}
| Asset type | Use when | KYC? | Liveness? | Cost | Enroll via |
|---|---|---|---|---|---|
| Virtual Portrait | AI character, mascot, avatar | No | No | $0.012 USDC | POST /v1/portrait/enroll · studio/portrait |
| RealFace | Real person you have rights to | No | Yes (~1 min on phone) | $0.012 USDC (promo) | POST /v1/realface/init + /enroll · studio/realface |
Timing
| Phase | Typical latency |
|---|---|
POST → upstream job submitted (202) | ~3–20s |
| Polling until clip ready | 60–180s (poll every 5–10s) |
| GCS backup + settle on the completed poll | ~1–30s |
Set your HTTP client timeout to at least 180s per poll. The POST handler itself caps the upstream submit at ~20s (returns 504, no charge, if upstream doesn't acknowledge).
Error codes
| Code | Where | Description |
|---|---|---|
| 400 | POST / GET | Invalid request — bad/missing prompt, unsupported image_url/real_face_asset_id for the model, duration_seconds above max or not in the model's allowed set, model/provider mismatch on the poll. |
| 402 | POST / GET | Payment required (no header → x402 challenge), or payment verify/settle failed. On a completed-but-unsettleable poll, the clip was generated but the signed authorization could not be settled (often expired) — retry the poll. |
| 400 | POST | Content policy violation (Content policy violation). |
| 429 | POST / GET | Upstream rate limit. Response includes Retry-After (and X-RateLimit-Source). |
| 500 | POST / GET | Server / provider configuration error. |
| 504 | POST | Upstream submit timed out (>~20s). No payment taken — retry. |
| 504 | GET | Upstream poll timed out — retry the poll in a few seconds. |
What's next?
Zero-KYC ta_xxx enrollment for AI-character consistency across clips.
Real-person likeness with on-phone liveness — still no KYC.
Generate stills you can then seed into image-to-video.
Also useful: Music Generation · Error Handling · Real-person video walkthrough.