BlockRun
Get started

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:

1
Submit the job

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.

2
Poll until completed

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

No charge on timeout or failure

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:

GuaranteeDetail
No charge on failureIf the upstream job fails or you never poll, no USDC moves. Settlement only fires on a completed poll.
Wallet binding, not signature equalityThe 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-pollsPolling an already-settled job returns the same video URL again (payment.status: "already_settled") — you are never double-charged.
Replay-protectedEach signed authorization can submit exactly one job (nonce claim on POST).
Durable outputThe 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.
SDKs and ClawRouter hide all of this

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 IDNameDurations (sec)ResolutionImage-to-videoSynced audioCharacter / RealFace asset
azure/sora-2Sora 24 / 8 / 12 (default 4 — only these three)720p, portrait or landscape✅ (non-human only)
xai/grok-imagine-videoGrok Imagine Video1–15 (default 8)upstream default
bytedance/seedance-1.5-proSeedance 1.5 Prodefault 5, max 12720p (default)✅ (t2v)
bytedance/seedance-2.0-fastSeedance 2.0 Fastdefault 5, max 15720p (default)✅ (t2v)
bytedance/seedance-2.0 (Pro)Seedance 2.0 Prodefault 5, max 15720p (default), up to 4K✅ (t2v)

Notes:

  • Sora 2 accepts only duration_seconds of 4, 8, or 12 — any other value returns 400 listing the allowed set. Text-to-video only (no image_url). Output is 720p with synchronized audio, portrait or landscape.
  • Grok Imagine accepts duration_seconds from 1 to 15 (default 8); it accepts an optional image_url and 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-fast allow up to 15s, seedance-1.5-pro up to 12s (the gateway returns 400 for a duration_seconds above the tier max). The gateway bumps the default to 720p and sets generate_audio per the t2v/i2v split below. Only Seedance 2.0 / 2.0-fast accept a real_face_asset_id (ta_xxxx) for character/identity consistency, multiple reference images, and reference video/audio (r2v). seedance-2.0-fast finishes 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) on azure/sora-2, Grok, or any Seedance model. For azure/sora-2 the 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_xxxx asset — enroll the person once with their consent (RealFace, ~1-min on-phone liveness, $0.012), then pass real_face_asset_id. Details in Character consistency below.
  • An AI character / mascot: same flow with a Virtual Portrait asset (no KYC, $0.012).
Sora reference images cannot contain human faces

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)

ParameterTypeRequiredDescription
modelstringNoVideo model ID (default xai/grok-imagine-video). See table above.
promptstringYesText description of the video to generate.
image_urlstring (URL)NoSeed 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_idstringNoCharacter/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_urlstring (URL)NoFinal-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_urlsstring[]NoMultiple reference images for character/style consistency (cited as "image 1", "image 2"). seedance-2.0 / seedance-2.0-fast only.
reference_videosarray of {url}NoReference 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_audiosarray of {url}NoReference 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_typestringNoOptional 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_secondsintegerNoDuration 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.
resolutionstringNo360p / 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_ratiostringNoadaptive / 16:9 / 9:16 / 1:1 / 4:3 / 3:4 / 21:9 / 9:21. Seedance only — ignored by Grok.
generate_audiobooleanNoSynced audio track. Seedance default: true for text-to-video, false for image/face-conditioned. Pass explicitly to override. Ignored by Grok.
seedintegerNoReproducibility seed (Seedance). Same seed + prompt + params ≈ same clip.
watermarkbooleanNoEmbed the upstream Seedance watermark. Off by default at the gateway.
return_last_framebooleanNoAlso 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.

ModelBilling basisEffective 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-proToken-metered ($4.32 / M tokens)5s ≈ $0.49 · 12s ≈ $1.18
bytedance/seedance-2.0-fastToken-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):

ActionEndpointPrice
Virtual Portrait enrollmentPOST /v1/portrait/enroll$0.012 USDC per asset (no KYC)
RealFace enrollmentPOST /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

FieldTypeDescription
idstringComposite job id "{provider}:{upstreamId}"
statusstringqueuedin_progresscompleted | failed
data[].urlstringPermanent BlockRun-hosted URL (GCS-backed). Falls back to upstream URL if backup fails.
data[].source_urlstringOriginal upstream URL (may expire)
data[].duration_secondsintegerDuration of the generated clip
data[].request_idstringUpstream request id for debugging
data[].backed_upbooleantrue when mirrored to BlockRun's GCS bucket
payment.statusstringsettled | already_settled
payment.tx_hashstringOn-chain USDC settlement tx (also in X-Payment-Receipt header)
usageobjectOn 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 typeUse whenKYC?Liveness?CostEnroll via
Virtual PortraitAI character, mascot, avatarNoNo$0.012 USDCPOST /v1/portrait/enroll · studio/portrait
RealFaceReal person you have rights toNoYes (~1 min on phone)$0.012 USDC (promo)POST /v1/realface/init + /enroll · studio/realface

Timing

PhaseTypical latency
POST → upstream job submitted (202)~3–20s
Polling until clip ready60–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

CodeWhereDescription
400POST / GETInvalid 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.
402POST / GETPayment 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.
400POSTContent policy violation (Content policy violation).
429POST / GETUpstream rate limit. Response includes Retry-After (and X-RateLimit-Source).
500POST / GETServer / provider configuration error.
504POSTUpstream submit timed out (>~20s). No payment taken — retry.
504GETUpstream poll timed out — retry the poll in a few seconds.

What's next?

Also useful: Music Generation · Error Handling · Real-person video walkthrough.