- 首頁
- Seedance API
Seedance API: endpoints, docs and early access
Seedancee's REST API lets you create Seedance generations, poll jobs and receive signed webhooks from your own code, using the same models, credits and refund rules as the workspace. API access is part of the Pro plan, and the API is in early access: the endpoints below are live and answer for Pro workspaces (and workspaces admitted to early access) once the API is switched on. Other requests get a clear 403: api_disabled while the API is off, plan_required when the workspace is not on Pro.
Request API access
Tell us your use case and expected monthly volume. API access comes with Pro; early access for other workspaces is granted in batches.
Overview
The Seedancee API is a thin, stable layer over the same generation pipeline the dashboard uses. You authenticate with an API key, submit a generation request that is validated against the model registry (an unsupported resolution, duration or reference count is rejected before any credits are reserved), then poll the job or receive a signed webhook when it finishes. Finished videos are copied to Seedancee storage and returned as signed URLs, so you never depend on provider links that expire after 14 days. The machine-readable OpenAPI 3.1 spec is at /openapi.json.
- Models: every active Seedance model (2.5, 2.0, 2.0 Fast, 2.0 Mini, 1.5 Pro) with capabilities and credit rates served from the registry.
- Modes: text-to-video, image-to-video (first and last frame), reference-to-video, and edit/extend where the model supports them.
- Credits: the same per-generation pricing as the workspace, drawn from the same balance, with automatic release on failure or cancellation.
- Plan rules: API generations follow the same plan rules as the workspace. Pro, the plan that includes the API, allows every model, resolution and duration the model supports, plus reference videos, edit and extend.
- Uploads: presigned uploads for reference images, videos, audio and first/last frames.
- Delivery: polling, or webhooks signed with HMAC-SHA256 and retried on failure.
Authentication and scopes
Create keys in the dashboard under Developer API; a workspace can hold up to 10 active keys. A key is shown once, stored only as a SHA-256 hash, can be named and revoked individually (a revoked key cannot be restored), and acts for your workspace. Send it as a bearer token. Missing, invalid, revoked or expired keys return 401; while the API is switched off every call returns 403 api_disabled; a workspace that is not on Pro (and not in early access) returns 403 plan_required; a key without the scope an endpoint needs returns 403 insufficient_scope. If the workspace leaves Pro, its keys stop working but stay listed so you can revoke them.
Authorization: Bearer sdk_live_...
Content-Type: application/json
Idempotency-Key: order-1234 # optional, on POST /videos/generations| Scope | Allows |
|---|---|
| models:read | GET /api/v1/models |
| uploads:write | POST /api/v1/uploads and /uploads/{id}/complete, GET /uploads/{id} |
| generations:write | Create and cancel generations, send webhook test events |
| generations:read | Get and list generations |
| credits:read | GET /api/v1/credits |
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /models | Active models with snake_case capabilities and credit rates |
| POST | /uploads | Presigned upload for an image, video or audio reference |
| POST | /uploads/{id}/complete | Confirm the upload so it can be used as a reference (images are usually ready at once; videos and audio are verified first) |
| GET | /uploads/{id} | Upload status while the file is verified: poll until ready or failed |
| POST | /videos/generations | Create a generation (validates, reserves credits, submits the job) |
| GET | /videos/generations/{id} | Status, signed output URLs and the credit settlement |
| GET | /videos/generations | List generations (limit, starting_after) |
| POST | /videos/generations/{id}/cancel | Cancel a job that has not reached the provider yet; credits are released |
| GET | /credits | Spendable balance and credits reserved by running jobs |
| POST | /webhooks/test | Send a signed ping to your webhook endpoints or a URL |
Create a generation
Model ids are Seedancee's stable ids (seedance-2.5, seedance-2.0, seedance-2.0-fast, seedance-2.0-mini, seedance-1.5-pro), never provider ids. References are upload ids from POST /uploads or public https URLs, grouped by role: first_frame, last_frame, images, videos and audio. Defaults: mode text-to-video, duration 5 (or "auto" where the model supports it), aspect_ratio 16:9, resolution 720p, generate_audio true, output_format mp4. metadata takes up to 16 string pairs and is echoed back on the job and in webhooks. Send an Idempotency-Key header so a retried request never creates a second job or charge.
{
"model": "seedance-2.5",
"mode": "image-to-video",
"prompt": "Slow push-in toward the window; furniture unchanged. Sound: quiet room tone.",
"duration": 8,
"aspect_ratio": "16:9",
"resolution": "720p",
"generate_audio": true,
"references": { "first_frame": "<upload id>" },
"webhook_url": "https://example.com/hooks/seedancee",
"metadata": { "order_id": "1234" }
}{
"id": "0b8f6c1e-5d7a-4c33-9f55-2f0d0b1a9c10",
"object": "video.generation",
"status": "queued",
"model": "seedance-2.5",
"credits_reserved": 600,
"pricing_version": "2026-09-measured-v2",
"created_at": "2026-09-30T10:12:00.000Z"
}Query a job
{
"id": "0b8f6c1e-5d7a-4c33-9f55-2f0d0b1a9c10",
"object": "video.generation",
"status": "succeeded",
"model": "seedance-2.5",
"duration": 8,
"resolution": "720p",
"credits_reserved": 600,
"credits_captured": 600,
"outputs": [
{ "url": "https://…signed URL, valid 1 hour…", "format": "mp4", "duration": 8, "resolution": "720p" }
],
"error": null,
"metadata": { "order_id": "1234" },
"completed_at": "2026-09-30T10:15:41.000Z"
}Status values: queued, processing, succeeded, failed, cancelled. credits_captured is null while the job runs, the final charge on success, and 0 on failure or cancellation (the reservation is released). Output URLs are signed for one hour; fetch the job again for a fresh URL. When you set return_last_frame, last_frame_url links the video's final frame as an image, ready to use as the first frame of the next shot. You only ever see generations of your own workspace.
Two kinds of failure are not always final: if the provider does not confirm a submission (error code submit_unconfirmed) or does not finish in time (timeout), the reservation is released, but the video may still arrive (we accept the provider's late answer, and keep checking timed-out jobs for 24 hours). If it arrives, the job becomes succeeded, is charged then, and a generation.succeeded webhook follows. Do not resubmit such a job right away, or it may render and be charged twice. The create call itself can answer 202 with status failed, credits_reserved 0 and error.code submit_unconfirmed; no generation.failed webhook is sent in that case.
Webhooks
Add up to 10 webhook endpoints in the dashboard (each gets its own whsec_ signing secret, shown once), or pass webhook_url per request, which is signed with your workspace's per-request secret. Events: generation.succeeded, generation.failed and generation.cancelled. The body is { id, type, created, data: { object } } where object is the same JSON as the query endpoint; id stays the same across retries, so use it to deduplicate. Respond with 2xx within 10 seconds. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours; redirects are not followed; only public https URLs are accepted. POST /api/v1/webhooks/test sends a signed ping. If the workspace leaves Pro, generations created before still deliver to enabled endpoints; you can still disable or delete endpoints in the dashboard.
Seedancee-Signature: t=1790000000,v1=<64 hex characters>
v1 = hex(HMAC-SHA256(signing_secret, t + "." + raw_request_body))import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySeedanceeSignature(rawBody: string, header: string, secret: string, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // reject replays
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
// Use the raw body, not re-serialized JSON:
// const ok = verifySeedanceeSignature(await req.text(), req.headers.get("seedancee-signature")!, process.env.SEEDANCEE_WEBHOOK_SECRET!);import hashlib, hmac, time
def verify_seedancee_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Examples
curl -X POST https://seedancee.com/api/v1/videos/generations \
-H "Authorization: Bearer $SEEDANCEE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: espresso-001" \
-d '{"model":"seedance-2.0-mini","mode":"text-to-video","prompt":"Vertical video. Espresso pours over ice in a clear glass, crema swirling, hard morning sunlight. Sound: the pour, ice clinking.","duration":5,"aspect_ratio":"9:16","resolution":"720p","generate_audio":true}'
curl https://seedancee.com/api/v1/videos/generations/$GENERATION_ID \
-H "Authorization: Bearer $SEEDANCEE_API_KEY"import { readFile } from "node:fs/promises";
const API = "https://seedancee.com/api/v1";
const auth = { Authorization: `Bearer ${process.env.SEEDANCEE_API_KEY}`, "Content-Type": "application/json" };
// 1. Upload the first frame
const file = await readFile("frame.png");
const upload = await (await fetch(`${API}/uploads`, {
method: "POST", headers: auth,
body: JSON.stringify({ type: "image", mime: "image/png", bytes: file.length, filename: "frame.png", model: "seedance-2.5" }),
})).json();
await fetch(upload.upload_url, { method: "PUT", headers: upload.upload_headers, body: file });
let up = await (await fetch(`${API}/uploads/${upload.id}/complete`, { method: "POST", headers: auth, body: "{}" })).json();
while (up.status === "validating") {
// Videos and audio (and some images) are verified before they can be used
await new Promise((r) => setTimeout(r, 3_000));
up = await (await fetch(`${API}/uploads/${upload.id}`, { headers: auth })).json();
}
if (up.status !== "ready") throw new Error(up.error?.message ?? "Upload rejected");
// 2. Create the generation
const job = await (await fetch(`${API}/videos/generations`, {
method: "POST",
headers: { ...auth, "Idempotency-Key": "living-room-001" },
body: JSON.stringify({
model: "seedance-2.5",
mode: "image-to-video",
prompt: "Slow push-in toward the window; furniture unchanged. Sound: quiet room tone.",
duration: 8,
resolution: "720p",
references: { first_frame: upload.id },
}),
})).json();
// 3. Poll (or use a webhook)
let gen = job;
while (gen.status === "queued" || gen.status === "processing") {
await new Promise((r) => setTimeout(r, 10_000));
gen = await (await fetch(`${API}/videos/generations/${job.id}`, { headers: auth })).json();
}
console.log(gen.status, gen.outputs[0]?.url, gen.credits_captured);import os, time, requests
API = "https://seedancee.com/api/v1"
headers = {"Authorization": f"Bearer {os.environ['SEEDANCEE_API_KEY']}"}
r = requests.post(
f"{API}/videos/generations",
headers={**headers, "Idempotency-Key": "dive-watch-001"},
json={
"model": "seedance-2.0",
"mode": "text-to-video",
"prompt": "A steel dive watch on wet basalt, slow tilt across the dial as sunlight sweeps the crystal. Sound: a distant wave.",
"duration": 8,
"aspect_ratio": "16:9",
"resolution": "1080p",
"generate_audio": True,
},
)
r.raise_for_status()
job = r.json()
while True:
gen = requests.get(f"{API}/videos/generations/{job['id']}", headers=headers).json()
if gen["status"] not in ("queued", "processing"):
break
time.sleep(10)
print(gen["status"], gen["outputs"], gen["credits_captured"])Errors
Every error has the same shape, { "error": { "type", "code", "message", "param" } }, where param names the offending field in the request's own snake_case (for example resolution or references.images[1]). Every response carries an X-Request-Id header; include it when you contact support.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_json / invalid_parameter | Malformed JSON, bad query parameter or Idempotency-Key |
| 401 | missing_api_key / invalid_api_key | No key, unknown key, or a revoked or expired key |
| 402 | insufficient_credits | Balance is lower than the quoted reservation (includes required and balance) |
| 402 | subscription_required | The workspace has no active subscription, so it cannot generate; nothing was reserved |
| 403 | api_disabled / plan_required / insufficient_scope | The API is not switched on yet, the workspace is not on Pro (or in early access), or the key lacks the scope |
| 403 | plan_required (with field) | The workspace's plan does not include this model, mode, reference video, resolution or duration (required_tier names the plan that does); nothing was reserved |
| 404 | not_found | Unknown generation or upload, or it belongs to another workspace |
| 409 | not_cancellable / idempotency_key_reused | Job already processing, or the key was reused with a different body |
| 413 | too_large / storage_quota_exceeded | The upload is above the size limit, or your workspace storage is full |
| 422 | content_mismatch | The uploaded bytes are not the declared file type |
| 422 | missing_parameter / unknown_parameter | Required field missing or an unrecognised field sent |
| 422 | resolution_unsupported, duration_out_of_range, … | The model does not support the requested settings or reference count |
| 422 | invalid_reference / invalid_webhook_url | Reference is not an upload id or https URL; webhook URL is not public https |
| 429 | rate_limited | Per-key limit, or the per-user limit on creating generations, exceeded; wait Retry-After seconds |
| 502 | provider_error | The upstream provider rejected the job; credits were released |
| 503 | generation_unavailable | Generation is temporarily unavailable; nothing was charged |
| 503 | tier_unavailable | We could not check the workspace's plan; nothing was charged. Retry after Retry-After seconds |
Rate limits and pricing
Each key has its own per-minute limit (20 requests per minute by default, shown in the dashboard, and never more than 120). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; above the limit the API returns 429 with Retry-After. Creating generations is also limited to 10 per minute per user, counted together with generations started in the dashboard; that 429 carries limit_scope user_generations. API generations use exactly the same credit pricing as the workspace: GET /api/v1/models returns each model's credit rates and pricing_version, every create response states credits_reserved, and failed or cancelled jobs release the reservation automatically. Usage per day and per endpoint is shown in the dashboard.
Pricing and credit estimator
The same credit rates apply to API generations.
Other programmatic access
The models are also available directly from Kie.ai, whose asynchronous jobs API with callbacks is what Seedancee uses under the hood. Kie's output URLs expire after 14 days, so plan to copy files to your own storage if you integrate with it directly. We link Kie's documentation below for completeness; Seedancee is not affiliated with Kie.ai or ByteDance.
常見問題
Is the Seedance API live on Seedancee?
It is in early access. The /api/v1 endpoints on this page are implemented; while the API is switched off they return 403 api_disabled to everyone. Once it is on, Pro workspaces (and workspaces admitted to early access) create a key under Developer API in the dashboard; other workspaces get 403 plan_required. Request early access through the contact page.
Is there a Seedance 2.0 API today?
Yes. Seedancee's API serves Seedance 2.0 (plus 2.0 Fast and 2.0 Mini), Seedance 2.5 and Seedance 1.5 Pro under stable ids such as seedance-2.0. Kie.ai also exposes hosted APIs for the same models, and ByteDance points developers to its own platform from its official model pages.
How much does the Seedance API cost?
API access is included in the Pro plan. API generations use the same credit pricing as the workspace and draw from the same balance. GET /api/v1/models returns each model's credit rates, and every create response states the credits reserved. See the pricing page for plans and the estimator.
Does the API validate my request before charging?
Yes. Requests are checked against the model registry (mode, resolution, duration, aspect ratio, reference limits) and credits are reserved only for valid requests. Failed or cancelled generations release the reservation automatically.
Does the API support webhooks?
Yes. Configure endpoints in the dashboard or pass webhook_url per request. Deliveries are signed with HMAC-SHA256 in the Seedancee-Signature header and retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. POST /api/v1/webhooks/test sends a signed ping.
How do I avoid duplicate generations when I retry?
Send an Idempotency-Key header on POST /api/v1/videos/generations. Repeating the request with the same key and body returns the original job without a second charge; reusing the key with a different body returns 409.
Does Seedancee expose provider model ids?
No. You use Seedancee's stable model ids such as seedance-2.5, so a provider change does not break your integration.