Seedance 2.0 FastAPI reference
Everything you need to call Seedance 2.0 Fast from code: a three-step quick start, every field with its limits, the pricing formula, webhooks and idempotency. The MCP tools run on the same /v1 API; signed in over MCP you spend the creative balance, with an API key the developer API balance.
Developer API access is by application.
Quick startin three calls
Set a key, send the job with a price cap, then read the result.
- 01
Set your key
Developer API access is by application: request it in the studio under API keys. A key is shown once, so keep it in an environment variable.
export API_KEY="sk_test_…" - 02
Send the job
POST the clip with an Idempotency-Key and a max_credits cap (or a quote_id). You get 202 and a queued job straight away, and the credits are held.
curl https://api.pershot.app/v1/videos/generations \ -H "Authorization: Bearer $API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0-fast", "prompt": "Perfume mist in hard side light. Slow push-in, black background, grain.", "duration": 5, "resolution": "720p", "aspect_ratio": "16:9", "generate_audio": true, "max_credits": 34 }'{ "id": "job_01J9ZK4T8W", "object": "job", "kind": "video", "status": "queued", "money_status": "reserved", "queue_position": 2, "requested_model": "seedance-2.0-fast", "credits_quoted": 34, "credits_charged": null } - 03
Read the result
GET the job until its status is succeeded. outputs[].url is signed and expires after 24 hours, so download what you keep.
curl https://api.pershot.app/v1/jobs/job_01J9ZK4T8W \ -H "Authorization: Bearer $API_KEY"
Parametersand limits
Read from the OpenAPI document, which is generated from the same schemas the API validates against. Unknown fields are refused.
| Field | Type | Allowed | Notes |
|---|---|---|---|
modelRequired | string | seedance-2.0-fast · seedance-2.0-mini · auto | The model id. |
promptRequired | string | 2–2,000 characters | What to render: the subject, the camera and the light. |
durationDefault 5 | integer | 4–15 | Output length in whole seconds, up to 15. |
resolutionDefault "720p" | string | 480p · 720p | Output resolution. 480p for drafts, 720p for delivery; each is its own render. |
aspect_ratioDefault "16:9" | string | 16:9 · 9:16 · 1:1 | Frame shape. |
generate_audioDefault true | boolean | – | Native audio. The price is the same on or off. |
image_urlOptional | string | https URL | First frame for video; the source image for the edit models. A public https URL or an upload URL. |
reference_image_urlsOptional | string[] | Up to 9 · https URL | Reference images for subject and style. Refer to them in the prompt as @image1, @image2. |
seedOptional | integer | 0–2,147,483,647 | Accepted and ignored: Seedance 2.0 takes no seed. |
quote_idOptional | string | qt_… | Run at a quote from POST /v1/quotes, valid for 10 minutes. |
max_creditsOptional | number | > 0 | The most this job may cost. Above it the job is refused, never charged. |
webhook_urlOptional | string | https URL | An https URL that receives the job's events. |
end_user_idOptional | string | 1–128 characters | For approved developer accounts (access is by application): your own user id for this request, shown in usage. |
project_idOptional | string | prj_… | |
shot_idOptional | string | sht_… | |
routing_preferenceOptional | string | stability · speed | |
enhanceOptional | object | – |
Send exactly one of quote_id or max_credits. This endpoint refuses model "auto": for video, call POST /v1/auto/plan first, then send the model it picks with max_credits.
- Rate and concurrency
- 60 requests a minute per key, and 3 jobs at a time per account by default. More keys do not add concurrency.
Limits
Read from the same table the API checks against. Each row says who checks the limit and what happens past it.
- EnforcedWe check it and answer 400 before anything is quoted or held.
- SupplierThe supplier checks it. A job it refuses fails, and nothing is charged.
- RecommendedNobody checks it. Staying inside it gives better results.
| Limit | Value | Unit | Who checks | Past the limit |
|---|---|---|---|---|
| Request | ||||
Prompt lengthprompt | 2 to 2,000 | characters | Enforced | 400 invalid_request on prompt, before any quote or hold. |
Prompt in Englishprompt | up to 1,000 | words | Recommended | Accepted. The model may follow a longer prompt less closely. |
Prompt in Chineseprompt | up to 500 | Chinese characters | Recommended | Accepted. The model may follow a longer prompt less closely. |
Durationduration | 4 to 15Default 5 | seconds | Enforced | 400 invalid_request on duration. Whole seconds only. |
Resolutionresolution | 480p, 720pDefault 720p | – | Enforced | 400 invalid_request on resolution. |
Aspect ratioaspect_ratio | 16:9, 9:16, 1:1Default 16:9 | – | Enforced | 400 invalid_request on aspect_ratio. |
Seedseed | Ignored | – | Supplier | Accepted and not sent: Seedance 2.0 takes no seed, so a seed does not repeat a result. |
Input video lengthinput_video_seconds | 2 to 15 | seconds | Enforced | 400 invalid_request on POST /v1/quotes. API generations take no input video. |
| Images you send | ||||
Reference imagesreference_image_urls | up to 9 | images | Enforced | 400 invalid_request on reference_image_urls. |
First frame or referencesreference_image_urls | image_url or reference_image_urls, not both | – | Enforced | 400 invalid_request on reference_image_urls. Seedance 2.0 runs one of the two modes per job. |
Image sideimage_urlreference_image_urls | 300 to 6,000 | px per side | Enforced on uploads | Uploads: 400 invalid_request on image_url or reference_image_urls.N, before any quote or hold. Other links: the supplier refuses the job, and nothing is charged. |
Image shapeimage_urlreference_image_urls | 0.4 to 2.5 | width ÷ height | Enforced on uploads | Uploads: 400 invalid_request on image_url or reference_image_urls.N, before any quote or hold. Other links: the supplier refuses the job, and nothing is charged. |
Upload sizefile | up to 10 | MB | Enforced | POST /v1/uploads answers 400 invalid_request on file. |
Upload formatsfile | JPEG, PNG, WebP | – | Enforced | Read from the file itself, not its name. Anything else: 400 invalid_request on file. |
Linked image sizeimage_urlreference_image_urls | up to 30 | MB | Supplier | For images linked from other hosts. The supplier refuses the job, and nothing is charged. |
Linked image formatsimage_urlreference_image_urls | JPEG, PNG, WebP, BMP, TIFF, GIF, HEIC, HEIF | – | Supplier | For images linked from other hosts. The supplier refuses the job, and nothing is charged. |
| Output | ||||
| Output pixels | 480p16:9864×4969:16496×8641:1640×640720p16:91280×7209:16720×12801:1960×960 | px | Supplier | Set by the model for each resolution and aspect ratio. |
| Frame rate | 24 | fps | Supplier | The same for every video. |
| Output format | MP4 | – | Supplier | Every video is an MP4 with sound when generate_audio is on. |
| Links | ||||
| Upload link | 7 | days | Enforced | After that the link answers 403. Upload the file again. |
| Output link | 24 | hours | Enforced | After that the link answers 403. GET the job again for a fresh link. The file itself is kept for 12 months (the job's outputs_expire_at gives the date), longer if the result is saved to Assets or used in a project. |
We never fetch images from other hosts, so for those links the supplier checks the size, and a job it refuses is not charged. The same limits come back as JSON: GET /v1/models returns a limits object for every model.
Pricingworked out in the open
Every job is quoted in credits before it runs (US$1 = 100 credits). The per-second rate is for budgeting; POST /v1/quotes returns the exact figure.
credits = ceil(BytePlus list price × 0.55 × 100), where list price = (input s + output s) × width × height × 24 ÷ 1024 × list rate| Spec | Per second (API) | 5 s | 10 s | 15 s |
|---|---|---|---|---|
| 480pNo video input | $0.031With an input video: $0.019/s≈ 322 s per $10 | 16 cr≈ $0.16 | 31 cr≈ $0.31 | 47 cr≈ $0.47 |
| 720pNo video input | $0.067With an input video: $0.040/s≈ 149 s per $10 | 34 cr≈ $0.34 | 67 cr≈ $0.67 | 100 cr≈ $1.00 |
Refused or failed jobs are not charged.
Webhooksand idempotency
Retries never charge twice, and you do not have to poll.
- Idempotency
- Every generation POST needs an Idempotency-Key (1 to 255 visible ASCII characters). The same key within 24 hours returns the same job with HTTP 200, so a retry after a timeout never creates or charges a second job.
- Refused prompts
- A prompt refused by the pre-check (422 content_rejected) creates nothing, so the same key can be used again with a new prompt.
- Events
job.queuedjob.processingjob.succeededjob.failedjob.cancelled- Signature
- X-Signature carries t, the unix time, and v1, an HMAC-SHA256 of t, a dot and the raw body, keyed with your whsec_ secret. Refuse timestamps older than five minutes and dedupe on X-Event-Id.
- Delivery
- A 2xx within 10 seconds counts as delivered. Failed deliveries retry after 1 min, 5 min, 30 min, 2 h and 6 h. Redirects are not followed.
{
"id": "evt_01J9ZKB1QX",
"type": "job.succeeded",
"created_at": "2026-09-29T14:02:31Z",
"data": {
"job": {
"id": "job_01J9ZK4T8W",
"object": "job",
"kind": "video",
"status": "succeeded",
"money_status": "settled",
"requested_model": "seedance-2.0-fast",
"credits_charged": 34,
"outputs": [
{
"kind": "video",
"url": "https://…/job_01J9ZK4T8W.mp4?sig=…"
}
]
}
}
}import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received, before any JSON parsing.
export function verifyWebhook(header, rawBody, secret, toleranceSec = 300) {
let t = null;
const signatures = [];
for (const item of (header ?? "").split(",")) {
const [key, value] = item.split("=", 2);
if (key === "t") t = Number(value);
if (key === "v1") signatures.push(Buffer.from(value, "hex"));
}
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((s) => s.length === expected.length && timingSafeEqual(s, expected));
}
// verifyWebhook(req.headers["x-signature"], rawBody, process.env.WEBHOOK_SECRET)
// Answer 2xx quickly, and skip an X-Event-Id you have already handled.Errorsand what to do
Every error has the same shape: an error object with code, message, param and request_id. The ones you are most likely to meet:
| Code | HTTP | What to do |
|---|---|---|
invalid_request | 400 | Fix the field named in param. |
unauthorized | 401 | Check the key and the Bearer header. |
insufficient_credits | 402 | Top up, then retry with the same Idempotency-Key. |
forbidden_model | 403 | Add the model or tool to the key's allowlist in the studio, or use another key. Tools such as Enhance need their own entry. |
quote_expired | 409 | Get a new quote; quotes last 10 minutes. |
price_changed | 409 | The price rose above your cap. Quote again or raise max_credits. |
content_rejected | 422 | Nothing was charged. Change the prompt or the images. |
rate_limited | 429 | Wait for Retry-After, then retry. |
upstream_unavailable | 503 | The model is unavailable. Retry later; a job that never starts is not charged. |