Sora2U Video Generation Open API
Through this API, any Sora2U user can use their own API key to call the website's video generation capability from their own program / script / server. Every generation is charged normally from that user's credit (GP) balance, following exactly the same billing rules as the web client.
- Base URL:
https://sora2u.com - Protocol: HTTPS; requests/responses are JSON (reference images for task creation are inlined as base64)
- Authentication: API key (
Authorization: Bearer <key>) - Style: tasks are asynchronous — creation returns a task ID first, then you poll for the result
The machine-readable interface definition is in
openapi.yamlin the same directory, which can be imported directly into Postman / Swagger UI / openapi-generator.
Table of Contents
- Quick Start (Up and Running in 5 Minutes)
- Obtaining and Managing API Keys
- Authentication
- Generating a Video
- Querying Task Status
- Other Endpoints
- Billing and Credit Deduction
- Task State Machine
- Error Codes
- Best Practices
- Complete Examples (Node.js / Python)
- For Maintainers: Deployment Steps
Quick Start (Up and Running in 5 Minutes)
# 0. First, log in on the web client and create a key (see the next section) to obtain a plaintext key of the form sk_sora_xxx
export SORA_KEY="sk_sora_your_key"
# 1. Query your balance to confirm you have credits
curl -s https://sora2u.com/api/v1/credits \
-H "Authorization: Bearer $SORA_KEY"
# 2. Create a text-to-video task
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"A corgi running on the beach, cinematic quality, golden-hour lighting","model":"seedance-2.0","duration":5}'
# => { "success": true, "task": { "id": "ckxxx", "status": "pending", ... } }
# 3. Poll for the result using the task.id returned in the previous step (once every 5 seconds)
curl -s https://sora2u.com/api/v1/videos/ckxxx \
-H "Authorization: Bearer $SORA_KEY"
# When status = "completed", video_url is the address of the finished videoObtaining and Managing API Keys
An API key is bound to your account and represents you when calling the API and consuming your credits. Keep it as safe as a password.
Creating / listing / revoking keys is done using the web client login session (cookie session), typically invoked by the console page:
Create a Key · POST /api/api-keys
Request body (all optional):
| Field | Type | Description |
|---|---|---|
name | string | A note name for the key, to help distinguish its purpose, up to 60 characters, default "Default Key" |
expires_in_days | number | Number of days valid (1–3650). If omitted, never expires |
Response (201) — the plaintext key is returned only this once, save it immediately:
{
"success": true,
"api_key": "sk_sora_AbC1d2Ef...full plaintext...",
"key": {
"id": "ckkey123",
"name": "My Script",
"key_prefix": "sk_sora_AbC1…",
"expires_at": null,
"created_at": "2026-06-17T12:00:00.000Z"
},
"warning": "Please keep this key safe; it is shown only once and cannot be viewed again."
}List Keys · GET /api/api-keys
Returns only the prefix and status, never the plaintext:
{
"success": true,
"keys": [
{
"id": "ckkey123",
"name": "My Script",
"key_prefix": "sk_sora_AbC1…",
"status": "active",
"last_used_at": "2026-06-17T12:30:00.000Z",
"expires_at": null,
"revoked_at": null,
"created_at": "2026-06-17T12:00:00.000Z"
}
]
}status values: active, expired, revoked.
Revoke a Key · DELETE /api/api-keys/{id}
Takes effect immediately; the operation is idempotent (revoking again also returns success). If a key is leaked, revoke and recreate it as soon as possible.
A single account can keep at most 20 active keys; to exceed this you must first revoke old keys.
Authentication
All /api/v1/* endpoints require the key in the request header. Two forms are supported (the first is recommended):
Authorization: Bearer sk_sora_your_keyx-api-key: sk_sora_your_keyAuthentication failures uniformly return 401:
{ "error": { "code": "unauthorized", "message": "Missing or invalid API key..." } }The server only stores the SHA-256 hash of the key, not the plaintext; therefore, once lost, a key can only be recreated, not recovered.
Cross-Origin Calls (CORS)
/api/v1/* has CORS enabled and can be called directly in the browser from any web origin (the "Base URL + API Key" direct-connect mode of OpenAI-compatible clients):
- All responses carry
Access-Control-Allow-Origin: *; - Before sending an
Authorizationheader or usingPOST/DELETE, the browser first sends anOPTIONSpreflight, which this API responds to correctly (204+Access-Control-Allow-*); - Authentication uses
Authorization: Bearer/x-api-key(not cookies), so any origin is allowed andcredentialsis not required.
Note: the key represents you for billing purposes. When used in a pure frontend (browser), the key is exposed to the end user — only do this in trusted scenarios (such as the user entering their own key); for public-facing pages, proxy the request through your own backend instead, and do not put the key in frontend code.
Generating a Video
POST /api/v1/videos
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | ✅ | Video description, at least 10 characters. May embed `< |
model | string | ❌ | Model name, default seedance-2.0 (see GET /api/v1/models) |
duration | number | ❌ | Duration (seconds). Automatically rounded and clamped to the model's supported range |
aspect_ratio | string | ❌ | Aspect ratio, e.g. 9:16, 16:9 (depending on model support) |
resolution | string | ❌ | Resolution, e.g. 720p |
mute / disable_audio | boolean | ❌ | Mute generation: when true, asks the engine to add no auto-soundtrack / emit no audio track, to avoid Seedance 2.0's auto-BGM tripping output_audio_copyright (copyright/sensitive) failures. Default false |
reference | string | ❌ | Inline base64 of reference material (image / video / audio), optionally with a data:<mime>;base64, prefix. Subject to the ~4.5MB request-body limit, suitable for images and smaller files. See the "Uploading Reference Material" section below |
reference_url | string | ❌ | A public https direct link to the reference material; the server downloads it and forwards it to the engine. Use this for larger videos / audio (bypasses the request-body size limit). When supplied together with reference, both are submitted together as references (counted against the same limits) |
references | string[] | ❌ | A base64 array of multiple reference files (images / videos / audio can be mixed: images up to 9, video and audio share one 3-slot pool (combined ≤ 3), total up to 12); each element follows the same rules as reference. See "Uploading Reference Material · Multiple References" |
reference_urls | string[] | ❌ | An array of public https direct links to multiple reference files (same limits as references; the two are counted together); each element follows the same rules as reference_url. Can be mixed with references |
image / image_base64 | string | ❌ | Backward-compatible aliases for reference (image scenario) |
Response (202 Accepted) — the task has been created and credits have been pre-deducted, and background generation has begun:
{
"success": true,
"task": {
"id": "ckxxx",
"status": "pending",
"model": "seedance-2.0",
"model_name": "Seedance 2.0",
"mode": "image-to-video",
"duration": 5,
"estimated_credits": 100,
"estimated_time": "2-6 minutes",
"created_at": "2026-06-17T12:00:00.000Z"
},
"links": { "self": "/api/v1/videos/ckxxx" }
}Note:
estimated_creditsis the pre-deducted amount; final settlement is based on the actual cost when the task completes (with the difference refunded or charged, see Billing).
Example (image-to-video, compatible with the image field and bare base64):
B64=$(base64 -i ./ref.png | tr -d '\n')
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d "{\"prompt\":\"Make the character in the image slowly turn their head and smile\",\"model\":\"seedance-2.0\",\"image\":\"$B64\"}"Smart Model Selection (Automatic Text-to-Video When No Reference Is Provided)
Video models work in two forms: image-to-video / video-to-video / audio-driven (which need reference material) and text-to-video (pure text, no material needed). You do not need to, and cannot, separately specify the internal text-to-video model — the backend decides automatically based on whether reference material is present:
- Reference material provided (any of
reference/reference_url/references/reference_urls) → image-to-video / video-to-video / audio-driven with the model you selected; - No reference material provided at all → it automatically switches to the same family's text-to-video form and generates the video directly from
prompt.
In other words, to generate a video from pure text, simply omit all reference fields (use seedance-2.0 for model, or omit it for the default). Billing, duration, and aspect ratio are all settled according to the form that actually takes effect. Models with supports_text_only: true in GET /api/v1/models are the ones that "can generate without any material".
What you see in the response. Whether or not the auto-switch happens, the
model/model_namein the response is always the visible model you selected (e.g.seedance-2.0) — you will never see any internal / hidden fallback model name. The response also includes amodefield that states the actual form of this generation:text-to-video(no reference provided, auto text-to-video),image-to-video(an image / video / audio reference was provided), orimage-generation(image models). So gettingtext-to-videoback when you sent no reference is expected behavior — it does not mean the API can't accept uploads; when you do upload a reference, it becomesimage-to-video. Themodefield is present in both the create response and the task-status response.
# Text-to-video: just send no reference field at all; the backend goes text-to-video automatically
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Cyberpunk city at night, neon-lit rainy streets, camera slowly pushing in","model":"seedance-2.0","duration":5}'Model capability overview
Authoritative source is GET /api/v1/models; the table below is a quick reference for common models' reference-material capabilities:
Model (model) | Output | Text-to-video | Image ref | Video ref | Audio ref |
|---|---|---|---|---|---|
seedance-2.0 | video | ✅ | ✅ | ✅ | ✅ |
seedance-2.0-character | video | ✅ | ✅ | ✅ | ✅ |
seedance-1.5 | video | ✅ | ✅ | ❌ | ❌ |
gemini-image / kontext-image | image | ✅ | ✅ | ❌ | ❌ |
- Full-modal (
seedance-2.0/seedance-2.0-character): text / image / video / audio references all supported. - Image-only (
seedance-1.5): image references only; sending video/audio references returns400 unsupported_referenceat create time. - Image generation (
gemini-image/kontext-image): image references only (up to 4), no video/audio.
Uploading Reference Material (Image / Video / Audio)
POST /api/v1/videos supports three kinds of reference material: image (image-to-video), video (video-to-video), and audio (audio-driven).
There are two ways to hand the material to the endpoint; choose one based on file size:
| Method | Field | Suitable for | Limit | Description |
|---|---|---|---|---|
| Inline base64 | reference | Images, smaller short videos / audio | ~4.5MB per request body (platform hard limit) | Put the base64 directly into the JSON. Simple, but large files trigger 413 FUNCTION_PAYLOAD_TOO_LARGE |
| Remote direct link | reference_url | Larger videos / audio | Image 10MB, video 100MB, audio 50MB | Host the file at a public https address; the server downloads it and forwards it to the engine, bypassing the request-body limit |
General rules:
- Type capabilities are determined by the model: the
supports_image/supports_video/supports_audiofields ofGET /api/v1/modelsindicate which reference types each model supports.seedance-2.0(Seedance 2.0) supports image / video / audio;seedance-1.5(Seedance 1.5) supports image only. Passing a reference kind the model doesn't support returns400 unsupported_referenceat create time (e.g. sending video/audio references toseedance-1.5), rather than failing later during render. - ⚠️ Reference IMAGES must be real, content-bearing photos. Do not use solid-color blocks, placeholder images, or obviously synthetic / AI-generated images to "test the limit". Such low-information images trip the engine's low-quality content audit and fail with error code
10001305(often shown as "community guidelines") — this is NOT a real content violation; the same number of real photos passes fine. Testing with placeholder images fails 100% of the time and misleads you about the true cap. - Video reference must be ≥ 300px wide and < 15s (audio also < 15s):
- Width < 300px: for MP4/MOV videos this is pre-checked at create and returns
400 invalid_reference(message like "video width must be ≥ 300px (got ~200px)"); containers that can't be pre-parsed (e.g. WebM) are still enforced by the engine at render time. - Duration > 15s: via
reference_urlit returns400 invalid_reference_urlsynchronously at create (message likeVideo is 18.0s — reference must be under 15s.); viareference(base64) it fails during background processing,errorgives the reason, and reserved credits are auto-refunded.
- Width < 300px: for MP4/MOV videos this is pre-checked at create and returns
- When sending video / audio as base64, always use a data URL: bare base64 has no type information and will be treated as an image. That is,
referenceshould be written asdata:video/mp4;base64,...ordata:audio/mpeg;base64,.... - When both
referenceandreference_urlare provided, both are submitted together as references (merged withreferences/reference_urlsinto one set and counted against the same limits). - Multiple references (combined): use
references(base64 array) /reference_urls(direct-link array) to send several files at once; the engine takes them together as reference input. Video generation (e.g.seedance-2.0) supports mixed references: images ≤ 9 (stackable) + video & audio sharing one 3-slot pool (combined ≤ 3) + total ≤ 12 (e.g. 9 images + 3 videos = 12, or 7 images + 2 videos + 1 audio = 10); image-generation models only accept image references, up to 4. Exceeding a limit returns400 too_many_references. Singular and plural fields can be mixed and are merged into one set, in the submission orderreference_urls → references.
Supported common formats: images png/jpg/jpeg/webp, videos mp4/mov/webm, audio mp3/wav/m4a (based on Content-Type).
⚠️ Reference-material gotchas cheat-sheet
- Use real photos for image references, not solid-color/placeholder/synthetic images (else the low-quality audit
10001305fires, misreported as "community guidelines").- base64 video/audio must carry the prefix
data:video/mp4;base64,/data:audio/mpeg;base64,, else it's treated as an image.- Video reference width ≥ 300px and duration < 15s; audio duration < 15s.
- Counts: images ≤ 9, video + audio share one 3-slot pool (combined ≤ 3), total ≤ 12.
202only means accepted, not success — you must poll untilstatus = completedwith a non-emptyvideo_url; on failure readerror/error_code/retryable(see "Error codes").
A. Image (base64, the most common)
B64=$(base64 -i ./ref.png | tr -d '\n')
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d "{\"prompt\":\"Make the character in the image slowly turn their head and smile\",\"model\":\"seedance-2.0\",\"reference\":\"data:image/png;base64,$B64\"}"B. Video / audio (large files, use reference_url, recommended)
# First host the < 15-second reference video at any public https address (your OSS / S3 / CDN, etc.)
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Continue the camera movement of the reference video and keep pushing the shot in",
"model": "seedance-2.0",
"reference_url": "https://your-cdn.com/clips/ref-12s.mp4"
}'# Audio-driven works the same way
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"Have the character speak in rhythm with this audio","model":"seedance-2.0","reference_url":"https://your-cdn.com/clips/voice-10s.mp3"}'C. Video / audio (small files, inline base64)
B64=$(base64 -i ./ref.mp4 | tr -d '\n') # After decoding it must be < ~3MB, otherwise use reference_url instead
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d "{\"prompt\":\"Continue the camera movement of the reference video\",\"model\":\"seedance-2.0\",\"reference\":\"data:video/mp4;base64,$B64\"}"D. Multiple reference images (combined, up to 9)
Use references (a base64 array) to send several images at once; you can also use reference_urls for multiple direct links, or mix the two (up to 9 images; you can also mix in video/audio; video and audio share one 3-slot pool (combined ≤ 3)).
A=$(base64 -i ./char.png | tr -d '\n') # character reference
B=$(base64 -i ./scene.png | tr -d '\n') # scene reference
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d "{\"prompt\":\"Place this character into this scene, walking naturally\",\"model\":\"seedance-2.0\",\"references\":[\"data:image/png;base64,$A\",\"data:image/png;base64,$B\"]}"# Use multiple public direct links (good for larger images); can be mixed with references
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Blend these two reference images into one coherent camera move",
"model": "seedance-2.0",
"reference_urls": ["https://your-cdn.com/a.jpg", "https://your-cdn.com/b.jpg"]
}'Download behavior and limits for
reference_url: onlyhttpsis accepted; domains that resolve to internal / loopback addresses are rejected, and no redirects of any kind (301/302/307, etc.) are followed — please provide a directly accessible direct link with no redirects. The download timeout is 120 seconds per file (sized to accommodate video references up to 100MB). The server downloads withUser-Agent: Sora2U-Reference-Fetcher/1.0— do not block this UA at the origin. If the origin returnsapplication/octet-streamor omitsContent-Type, the type is auto-detected from the file's magic bytes (rejected only if detection fails). Temporary signed URLs with query parameters are supported, but make sure their validity covers the entire generation process (≥ 1 hour recommended) — the generation engine may fetch the material again during the render phase, and an expired signature at that point fails the task withreference_download_failed.Per-file error attribution: when a download / validation / upload fails, the API returns
400 invalid_reference_url(413 reference_too_largefor oversized base64), and the error body carries afailed_referencefield:{ "index": <0-based index: within the same-source (url / base64) array for this endpoint; within the content array for the Volcano-compatible /v1/contents/generations/tasks endpoint>, "source": "url" | "base64", "url_host": "<origin host only, the full signed URL is never echoed>", "http_status": <HTTP status returned by the origin, download failures only>, "reason": <machine-readable failure category, download failures only: network / timeout / http / invalid_url / unsafe_url / unsupported_type / too_large> }, so you can pinpoint exactly which file failed and why.
E. Positioning references inside the prompt (<|media:N|> tokens)
By default (no tokens in the prompt) the engine auto-places each reference — an audio reference, for example, is automatically appended at the end of the sentence. That covers most use cases.
When you need to pin a reference at an exact spot in the sentence (e.g. "she says this audio clip, then turns away while the background stays on this image"), embed <|media:N|> positioning tokens in prompt:
- N is the 0-based index of the reference in this request's submitted reference list — "submission order = N" is the only hard constraint.
- The server and the engine pass
<|media:N|>through verbatim, never rewriting it; references without a token are still auto-placed by the engine. - To avoid computing merged order across fields, put all references in one array field (all in
reference_urls, or all inreferences) — then N is simply the array index. When fields are mixed, the merge order is: URL-type first (reference_url→reference_urls), then base64-type (reference→references). - With a single reference, it is
<|media:0|>.
# image (index 0) + audio (index 1): the character speaks the audio while the frame stays anchored to the image
curl -s -X POST https://sora2u.com/api/v1/videos \
-H "Authorization: Bearer $SORA_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "She says <|media:1|> and then turns away, keeping the look of <|media:0|>",
"model": "seedance-2.0",
"reference_urls": ["https://your-cdn.com/char.png", "https://your-cdn.com/voice-10s.mp3"]
}'Querying Task Status
GET /api/v1/videos/{id}
You can only query your own tasks. When a task is pending/processing, this endpoint also triggers a reconciliation along the way to
fetch the latest progress.
{
"success": true,
"task": {
"id": "ckxxx",
"status": "completed",
"progress": 100,
"progress_text": "Generation complete",
"prompt": "A corgi running on the beach...",
"model": "seedance-2.0",
"mode": "text-to-video",
"image_url": null,
"video_url": "https://.../result.mp4",
"error": null,
"error_code": null,
"retryable": null,
"estimated_credits": 100,
"reserved_credits": 0,
"credits_charged": 96,
"created_at": "2026-06-17T12:00:00.000Z",
"updated_at": "2026-06-17T12:04:10.000Z",
"completed_at": "2026-06-17T12:04:10.000Z"
}
}Failed-task example (error is a sanitized safe message; branch on error_code / retryable, see "Error codes · Task-failure error codes"):
{
"success": true,
"task": {
"id": "ckxxx",
"status": "failed",
"video_url": null,
"error": "Content audit failed. This usually happens with solid-color / synthetic / placeholder reference images that trip low-quality detection; retry with real, content-bearing images.",
"error_code": "video_audit_rejected",
"retryable": true
}
}202≠ success: a202from create only means accepted; you must poll untilstatus = completedwith a non-emptyvideo_url.status = completed→ fetch the result fromvideo_url.status = failed→ readerror(safe message) /error_code(stable code) /retryable; reserved credits already refunded.- All fields live under
task(not top-level);video_urlis sanitized and carries no internal session parameters.
How long does video_url stay valid?
- Normal case (vast majority): when a task completes, the platform archives the video to its own object storage. The
video_urlthen looks likehttps://.../api/files/videos/<id>.mp4?token=...and is valid long-term (the token is a permanent signature for that file and does not expire). - Fallback case (rare): if archiving transiently fails,
video_urltemporarily falls back to an engine relay URL of the formhttps://.../api/video/<id>. That URL depends on an upstream temporary cache and is typically only valid for a few hours; the platform automatically retries archiving for up to 48 hours after completion, and once it succeeds the same task'svideo_urlis updated to the persistent address (old relay URLs also 302-redirect to it automatically). - If a relay URL expires before archiving succeeds, requests return
410with error codevideo_expired(the upstream message isVideo expired (could not be cached in time)). At that point the video can no longer be retrieved — you must generate again. - Recommendation: download the result promptly (within 24 hours) after completion, or copy it to your own storage. Do not treat
video_urlas a permanent CDN hotlink.
Cancel a Task · DELETE /api/v1/videos/{id}
Only pending / processing tasks can be canceled; after cancellation the pre-deducted credits are refunded:
{ "success": true, "refunded": true, "message": "Task canceled, credits refunded." }Other Endpoints
List Recent Tasks · GET /api/v1/videos?limit=20
Returns your most recent task list; limit ranges from 1–50, default 20.
{ "success": true, "count": 2, "data": [ { "id": "...", "status": "completed", ... } ] }Models and Billing Rules · GET /api/v1/models
The response follows the OpenAI-compatible list envelope: top-level object: "list" + a data array, each model with object: "model",
whose id is the public model name (used as the model input parameter when creating a task), without the internal engine id. It also includes default_model
and credit_rule so you can grab the full context at once.
{
"object": "list",
"data": [
{
"id": "seedance-1.5",
"object": "model",
"created": 1735689600,
"owned_by": "sora2u",
"name": "Seedance 1.5",
"pricing_unit": "second",
"credit_cost_per_second": 10,
"credit_cost": 10,
"durations": [5, 8, 10, 12],
"default_duration": 5,
"aspect_ratios": ["9:16"],
"default_aspect_ratio": "9:16",
"resolutions": ["720p"],
"default_resolution": "720p",
"max_prompt_length": 2000,
"supports_image": true,
"supports_video": false,
"supports_audio": false,
"reference_max_seconds": 15,
"estimated_time": "2-6 minutes"
},
{
"id": "seedance-2.0",
"object": "model",
"created": 1735689600,
"owned_by": "sora2u",
"name": "Seedance 2.0",
"credit_cost_per_second": 20,
"supports_image": true,
"supports_video": true,
"supports_audio": true,
"reference_max_seconds": 15,
"...": "..."
}
],
"default_model": "seedance-2.0",
"credit_rule": {
"currency": "GP",
"gp_per_video_second": 10,
"min_charge_gp": 1,
"note": "Pre-deduction = credit_cost_per_second × duration; after the task completes, settlement is based on the actual cost, with the difference refunded or charged."
}
}Query Balance · GET /api/v1/credits
{
"success": true,
"balance": 1280,
"currency": "GP",
"daily_free_video": { "eligible": false, "remaining_today": 0, "note": "..." }
}Billing and Credit Deduction
Video generation consumes GP credits, and the entire flow is consistent with the web client:
- Pre-deduction (when creating a task): deducts the balance atomically by
pre-deducted credits = model's credits per second × duration (seconds). If the balance is insufficient,402 insufficient_creditsis returned directly and no task is created. When creating multiple tasks concurrently, the deduction is atomic and will not drive the balance negative. - Settlement (when the task completes): the actual cost from the upstream engine is converted to credits and reconciled against the pre-deduction —
if the actual cost is higher, the difference is charged; if lower, the difference is refunded. The final charge is reflected in the task's
credits_charged. - Refund (when the task fails / is canceled): the pre-deducted credits are fully refunded.
About the daily free quota: historical paying users on the web client get 1 free video quota per day. To prevent scripts from quietly consuming this quota through bulk calls, the open API does not use the free quota by default and always charges credits.
To top up credits, please go to the purchase page on the web client (the open API does not handle payments).
Task State Machine
pending ──▶ processing ──▶ completed
│ │
└─────────────┴────────▶ failed (pre-deducted credits automatically refunded)
│
└──(DELETE)──────────────▶ canceled (pre-deducted credits automatically refunded)| Status | Meaning | Next step |
|---|---|---|
pending | Created, queued | Poll |
processing | Generating | Poll, can read progress |
completed | Complete | Retrieve video_url |
failed | Failed | Read error, credits refunded |
progressis a stage milestone, not a linear percentage: 5 = created, 20 = uploading reference material, 35 = submitting to the generation engine, 55 = accepted by the engine, 70 = rendering, 100 = completed. 70 is held until the task reaches a terminal state — staying unchanged for several minutes during rendering is normal. For failed tasks,progressstops at the stage where the failure occurred: 35 ≈ failed during submission, 70 ≈ failed during rendering, which you can use to segment failure statistics by stage.⚠️ The
video_urlreturned while a task ispending/processingis a proxy address pre-written at submission time — it is not the finished video and cannot be downloaded; the only criterion for success isstatus === "completed"(whenfailed,video_urlis alwaysnull).
Error Codes
Errors are uniformly { "error": { "code", "message", ... } }.
| HTTP | code | Description |
|---|---|---|
| 400 | invalid_json | The request body is not valid JSON |
| 400 | invalid_prompt | Missing prompt or fewer than 10 characters |
| 400 | invalid_model | The model name does not exist |
| 400 | invalid_param | Invalid parameter (e.g. duration is not a number) |
| 400 | invalid_reference | reference is not valid base64 |
| 400 | invalid_reference_url | reference_url is invalid / not https / points to an internal network / not downloadable (incl. timeout and size over limit) / type mismatch / video or audio over 15 seconds. The error body carries failed_reference attribution (index / url_host / http_status / reason) |
| 400 | unsupported_media | The reference file type is not supported (only image / video / audio) |
| 400 | unsupported_reference | The selected model does not support this reference kind (e.g. sending video/audio references to seedance-1.5) |
| 400 | too_many_references | Too many reference files (video generation: images ≤ 9, video + audio share one 3-slot pool (combined ≤ 3), total ≤ 12; image generation: up to 4 images) |
| 401 | unauthorized | Key missing / invalid / revoked / expired |
| 402 | insufficient_credits | Insufficient balance, with current_balance and required_credits |
| 404 | not_found | The task does not exist or does not belong to you |
| 409 | invalid_status | The task status changed and it cannot be canceled |
| 413 | reference_too_large | A base64 reference exceeds the size limit (image 10MB / video 100MB / audio 50MB, platform-side limit); an oversized URL reference returns 400 invalid_reference_url instead — both carry failed_reference attribution |
| 500 | internal_error | Server error, retryable |
The table above lists errors returned synchronously by create/query requests ({ "error": { "code", "message" } }).
Task-failure error codes (task.error_code)
After a task is accepted (202) but fails during generation, GET /api/v1/videos/{id} returns status = "failed" and normalizes the reason into stable fields: error (safe message), error_code (table below), retryable (whether an as-is retry is worthwhile). The raw upstream error string is sanitized and never leaks internal details.
error_code | Meaning | retryable |
|---|---|---|
video_audit_rejected | Content audit failed — usually low-quality detection triggered by solid-color/synthetic/placeholder reference images (error code 10001305 "community guidelines"), NOT a real violation; retry with real, content-bearing images | ✅ Yes |
input_image_real_person | The reference image appears to contain a real person and was blocked by the model's privacy protection. Retrying with the same image will be rejected again — switch to a non-real-person / cartoon / AI-generated reference image, or use seedance-2.0-character (anonymizes faces as sketches) | ❌ No |
output_audio_copyright | The generated soundtrack likely tripped copyright/sensitive detection (common with auto-added audio). Retry, or avoid specific music in the prompt | ✅ Yes |
content_policy | The prompt or reference material hit content policy; adjust and retry | ❌ No |
too_many_references | Too many reference items; reduce the count | ❌ No |
invalid_reference | Reference material invalid (video must be ≥300px wide, <15s, a real valid file); replace it | ❌ No |
reference_download_failed | The generation engine could not fetch your reference material URL during the render phase. Verify the link is publicly accessible and not expired (signed URLs should stay valid for ≥ 1 hour), then retry | ✅ Yes |
reference_upload_failed | Uploading the reference material to the generation engine failed (platform ↔ engine link issue); simply resubmit the task | ✅ Yes |
engine_retry_exhausted | The generation engine still failed after multiple internal attempts (usually 3), mostly transient upstream turbulence. Retry later; contact the platform if it persists | ✅ Yes |
platform_timeout | The task timed out on the platform side (queued / processing with no progress for a long time); it was auto-canceled and the reserved credits refunded. Retry directly; contact the platform if this happens frequently | ✅ Yes |
settlement_failed | Generation completed, but the actual cost was higher than the estimate and the balance was insufficient for the extra charge. Top up and submit again | ❌ No |
task_cancelled | The task was canceled by the caller and the reserved credits refunded; create a new task to continue | — |
engine_error | Any other unclassified engine processing failure (catch-all code) | ✅ Yes |
Retry advice:
retryable = truemeans an as-is retry may succeed (audit spot-checks, engine jitter, auto-soundtrack copyright, etc.) — use exponential backoff for 2–3 tries;retryable = falsemeans fix the request first (use real reference images, reduce count, adjust the prompt). Reserved credits for a failed task are automatically refunded in full.
Best Practices
- Polling cadence: we recommend querying the status once every 5–10 seconds, polling until a terminal state (completed / failed / cancelled) with a time budget of at least 30 minutes — complex tasks can take more than 20 minutes to finish, so do not declare failure early on a fixed 10-minute window (if you give up early and the task eventually completes, it is still charged normally). To abandon a task, first try canceling it with
DELETE /api/v1/videos/{id}(currently only supported for tasks that are queued / not yet submitted for rendering; on success the reserved credits are refunded).GET /api/v1/videos/{id}also triggers a reconciliation along the way. - Fetch results promptly: after
status = completed, downloadvideo_urlas soon as possible (within 24 hours) and copy it to your own storage; see "Query Task Status · How long doesvideo_urlstay valid?" for details. - Check the balance first: before bulk generation, call
GET /api/v1/creditsfirst to avoid a402midway. - Key security: keep it only in server-side environment variables, and never put it in frontend code / repositories / logs; use multiple keys by purpose, and revoke any that are leaked.
- Set an expiry: for temporary scripts, use
expires_in_daysto create short-lived keys, reducing the risk of leaks. - Error retries: only do exponential-backoff retries for
5xx;4xxare caller-side problems, so fix the parameters before retrying. - Rate limiting: the credit mechanism itself limits abuse (no credits means no generation). If you need to integrate a high-frequency system, please contact operations to negotiate a quota.
Complete Examples
Node.js (18+, native fetch)
const BASE = "https://sora2u.com";
const KEY = process.env.SORA_KEY;
const headers = {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
};
async function generate(prompt) {
const res = await fetch(`${BASE}/api/v1/videos`, {
method: "POST",
headers,
body: JSON.stringify({ prompt, model: "seedance-2.0", duration: 5 }),
});
if (!res.ok) throw new Error(`Creation failed: ${JSON.stringify(await res.json())}`);
const { task } = await res.json();
// Poll until complete
for (let i = 0; i < 120; i++) {
await new Promise((r) => setTimeout(r, 5000));
const s = await (
await fetch(`${BASE}/api/v1/videos/${task.id}`, { headers })
).json();
if (s.task.status === "completed") return s.task.video_url;
if (s.task.status === "failed") throw new Error(s.task.error);
}
throw new Error("Timed out");
}
generate("A corgi running on the beach, cinematic quality").then(console.log);Python (requests)
import os, time, requests
BASE = "https://sora2u.com"
H = {"Authorization": f"Bearer {os.environ['SORA_KEY']}"}
def generate(prompt: str) -> str:
r = requests.post(f"{BASE}/api/v1/videos", headers=H,
json={"prompt": prompt, "model": "seedance-2.0", "duration": 5})
r.raise_for_status()
task_id = r.json()["task"]["id"]
for _ in range(120):
time.sleep(5)
t = requests.get(f"{BASE}/api/v1/videos/{task_id}", headers=H).json()["task"]
if t["status"] == "completed":
return t["video_url"]
if t["status"] == "failed":
raise RuntimeError(t["error"])
raise TimeoutError()
print(generate("A corgi running on the beach, cinematic quality"))For AI Use: MCP and Skills
To make it easy for various AIs (Claude, Cursor, ChatGPT, etc.) to call this API directly, the repository ships with two ready-made integrations:
- MCP Server:
mcp/sora2u-video-mcp/. A stdio MCP server that exposes this API as tools:create_video(supports local image/video/audio as references),get_video,list_videos,cancel_video,list_models,get_credits. Suitable for MCP clients such as Claude Desktop, Cursor, and Cline; see the README in that directory for configuration. - Skill:
skills/sora2u-video/SKILL.md. A manual for use with Claude "Skills" / custom Agents, containing the complete calling flow and reference-material upload guide. - OpenAPI:
openapi.yamlin this directory is the machine-readable specification, which can be imported directly into ChatGPT "Actions" / custom GPTs, Apifox, and Postman as function/tool definitions.
All three cover the upload details of image / video / audio reference material (both the base64 and reference_url methods).
For Maintainers: Deployment Steps
This release adds an ApiKey data table and a set of /api/v1/* and /api/api-keys endpoints. Before deploying:
# 1. Install dependencies and generate the Prisma Client (already includes the new ApiKey model)
npm install
npx prisma generate
# 2. Apply database migrations (adds the ApiKey table)
npx prisma migrate deploy # production
# Or locally: npx prisma migrate dev
# 3. Build and start as usual
npm run build && npm startCode involved:
- Data model:
prisma/schema.prisma(ApiKey) + migrationprisma/migrations/20260617120000_add_api_keys - Key utilities:
lib/auth/api-key.ts(generation / hashing / authentication) - Generation service:
lib/ai/video-generation-service.ts(the billing pipeline shared by the in-site and open APIs) - Open endpoints:
app/api/v1/{videos,videos/[id],models,credits}/route.ts - Key management:
app/api/api-keys/route.ts,app/api/api-keys/[id]/route.ts - Response utilities:
lib/api/public-api.ts
The environment variables required for authentication are the same as for the existing video generation (
DATABASE_URL, video-engine-related configuration, etc.); the open API introduces no new required environment variables.
