Open API

Sora2U Video Generation API Docs

Authenticate with an API key and pay per credit to create video-generation tasks over a REST API. Full reference and details below.

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.yaml in the same directory, which can be imported directly into Postman / Swagger UI / openapi-generator.


Table of Contents

  1. Quick Start (Up and Running in 5 Minutes)
  2. Obtaining and Managing API Keys
  3. Authentication
  4. Generating a Video
  5. Querying Task Status
  6. Other Endpoints
  7. Billing and Credit Deduction
  8. Task State Machine
  9. Error Codes
  10. Best Practices
  11. Complete Examples (Node.js / Python)
  12. For Maintainers: Deployment Steps

Quick Start (Up and Running in 5 Minutes)

bash
# 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 video

Obtaining 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):

FieldTypeDescription
namestringA note name for the key, to help distinguish its purpose, up to 60 characters, default "Default Key"
expires_in_daysnumberNumber of days valid (1–3650). If omitted, never expires

Response (201) — the plaintext key is returned only this once, save it immediately:

json
{
  "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:

json
{
  "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):

http
Authorization: Bearer sk_sora_your_key
http
x-api-key: sk_sora_your_key

Authentication failures uniformly return 401:

json
{ "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 Authorization header or using POST / DELETE, the browser first sends an OPTIONS preflight, which this API responds to correctly (204 + Access-Control-Allow-*);
  • Authentication uses Authorization: Bearer / x-api-key (not cookies), so any origin is allowed and credentials is 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):

FieldTypeRequiredDescription
promptstringVideo description, at least 10 characters. May embed `<
modelstringModel name, default seedance-2.0 (see GET /api/v1/models)
durationnumberDuration (seconds). Automatically rounded and clamped to the model's supported range
aspect_ratiostringAspect ratio, e.g. 9:16, 16:9 (depending on model support)
resolutionstringResolution, e.g. 720p
mute / disable_audiobooleanMute 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
referencestringInline 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_urlstringA 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)
referencesstring[]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_urlsstring[]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_base64stringBackward-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:

json
{
  "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_credits is 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):

bash
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_name in 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 a mode field 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), or image-generation (image models). So getting text-to-video back 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 becomes image-to-video. The mode field is present in both the create response and the task-status response.

bash
# 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)OutputText-to-videoImage refVideo refAudio ref
seedance-2.0video
seedance-2.0-charactervideo
seedance-1.5video
gemini-image / kontext-imageimage
  • 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 returns 400 unsupported_reference at 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:

MethodFieldSuitable forLimitDescription
Inline base64referenceImages, 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 linkreference_urlLarger videos / audioImage 10MB, video 100MB, audio 50MBHost 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_audio fields of GET /api/v1/models indicate 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 returns 400 unsupported_reference at create time (e.g. sending video/audio references to seedance-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_url it returns 400 invalid_reference_url synchronously at create (message like Video is 18.0s — reference must be under 15s.); via reference (base64) it fails during background processing, error gives the reason, and reserved credits are auto-refunded.
  • 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, reference should be written as data:video/mp4;base64,... or data:audio/mpeg;base64,....
  • When both reference and reference_url are provided, both are submitted together as references (merged with references / reference_urls into 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 returns 400 too_many_references. Singular and plural fields can be mixed and are merged into one set, in the submission order reference_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 10001305 fires, 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.
  • 202 only means accepted, not success — you must poll until status = completed with a non-empty video_url; on failure read error / error_code / retryable (see "Error codes").

A. Image (base64, the most common)

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

bash
# 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"
      }'
bash
# 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)

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

bash
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\"]}"
bash
# 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: only https is 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 with User-Agent: Sora2U-Reference-Fetcher/1.0 — do not block this UA at the origin. If the origin returns application/octet-stream or omits Content-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 with reference_download_failed.

Per-file error attribution: when a download / validation / upload fails, the API returns 400 invalid_reference_url (413 reference_too_large for oversized base64), and the error body carries a failed_reference field: { "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 in references) — then N is simply the array index. When fields are mixed, the merge order is: URL-type first (reference_urlreference_urls), then base64-type (referencereferences).
  • With a single reference, it is <|media:0|>.
bash
# 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.

json
{
  "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"):

json
{
  "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: a 202 from create only means accepted; you must poll until status = completed with a non-empty video_url.
  • status = completed → fetch the result from video_url.
  • status = failed → read error (safe message) / error_code (stable code) / retryable; reserved credits already refunded.
  • All fields live under task (not top-level); video_url is 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_url then looks like https://.../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_url temporarily falls back to an engine relay URL of the form https://.../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's video_url is updated to the persistent address (old relay URLs also 302-redirect to it automatically).
  • If a relay URL expires before archiving succeeds, requests return 410 with error code video_expired (the upstream message is Video 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_url as 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:

json
{ "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.

json
{ "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.

json
{
  "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

json
{
  "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:

  1. 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_credits is returned directly and no task is created. When creating multiple tasks concurrently, the deduction is atomic and will not drive the balance negative.
  2. 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.
  3. 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

text
pending ──▶ processing ──▶ completed
   │             │
   └─────────────┴────────▶ failed        (pre-deducted credits automatically refunded)
   └──(DELETE)──────────────▶ canceled       (pre-deducted credits automatically refunded)
StatusMeaningNext step
pendingCreated, queuedPoll
processingGeneratingPoll, can read progress
completedCompleteRetrieve video_url
failedFailedRead error, credits refunded

progress is 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, progress stops 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_url returned while a task is pending / processing is a proxy address pre-written at submission time — it is not the finished video and cannot be downloaded; the only criterion for success is status === "completed" (when failed, video_url is always null).


Error Codes

Errors are uniformly { "error": { "code", "message", ... } }.

HTTPcodeDescription
400invalid_jsonThe request body is not valid JSON
400invalid_promptMissing prompt or fewer than 10 characters
400invalid_modelThe model name does not exist
400invalid_paramInvalid parameter (e.g. duration is not a number)
400invalid_referencereference is not valid base64
400invalid_reference_urlreference_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)
400unsupported_mediaThe reference file type is not supported (only image / video / audio)
400unsupported_referenceThe selected model does not support this reference kind (e.g. sending video/audio references to seedance-1.5)
400too_many_referencesToo many reference files (video generation: images ≤ 9, video + audio share one 3-slot pool (combined ≤ 3), total ≤ 12; image generation: up to 4 images)
401unauthorizedKey missing / invalid / revoked / expired
402insufficient_creditsInsufficient balance, with current_balance and required_credits
404not_foundThe task does not exist or does not belong to you
409invalid_statusThe task status changed and it cannot be canceled
413reference_too_largeA 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
500internal_errorServer 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_codeMeaningretryable
video_audit_rejectedContent 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_personThe 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_copyrightThe generated soundtrack likely tripped copyright/sensitive detection (common with auto-added audio). Retry, or avoid specific music in the prompt✅ Yes
content_policyThe prompt or reference material hit content policy; adjust and retry❌ No
too_many_referencesToo many reference items; reduce the count❌ No
invalid_referenceReference material invalid (video must be ≥300px wide, <15s, a real valid file); replace it❌ No
reference_download_failedThe 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_failedUploading the reference material to the generation engine failed (platform ↔ engine link issue); simply resubmit the task✅ Yes
engine_retry_exhaustedThe generation engine still failed after multiple internal attempts (usually 3), mostly transient upstream turbulence. Retry later; contact the platform if it persists✅ Yes
platform_timeoutThe 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_failedGeneration 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_cancelledThe task was canceled by the caller and the reserved credits refunded; create a new task to continue
engine_errorAny other unclassified engine processing failure (catch-all code)✅ Yes

Retry advice: retryable = true means an as-is retry may succeed (audit spot-checks, engine jitter, auto-soundtrack copyright, etc.) — use exponential backoff for 2–3 tries; retryable = false means 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, download video_url as soon as possible (within 24 hours) and copy it to your own storage; see "Query Task Status · How long does video_url stay valid?" for details.
  • Check the balance first: before bulk generation, call GET /api/v1/credits first to avoid a 402 midway.
  • 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_days to create short-lived keys, reducing the risk of leaks.
  • Error retries: only do exponential-backoff retries for 5xx; 4xx are 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)

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

python
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.yaml in 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:

bash
# 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 start

Code involved:

  • Data model: prisma/schema.prisma (ApiKey) + migration prisma/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.

视频生成 API 文档 | Sora2U | Sora2U — Free AI Video Generator