# Chenvoya API documentation This file holds every documentation page in reading order; each page also has its own Markdown file. --- ## Quickstart Generate your first video in three steps: create an API key, submit a task, get the result. You need an opened account with enough balance to reserve one generation. The example below is a Seedance 2.0 text-to-video request with the default duration, resolution and aspect ratio (5 seconds, 720p, adaptive). **API address**: Ask the person who opened your account for the API address. ### 1. Create an API key On the [API keys](./integration.html) page of the console, choose Create API key, then save it and the API address above in environment variables on your server. One API key works with every model. ```bash export CHENVOYA_BASE_URL="your-api-address" export CHENVOYA_API_KEY="your-api-key" ``` > **Warning**: Use API keys on servers only. Never put one in a web page, an app or a code repository, or paste it into documents or logs. If you suspect a leak, suspend the key in the console and create a new one. ### 2. Submit a generation task Send the model and the prompt to `POST /v1/video/generations`. Give every creation a new `Idempotency-Key`: if the request times out or the connection drops, retry with the same key and body to get exactly one task and one charge. ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "content": [ { "type": "text", "text": "A paper boat crossing a moonlit lake." } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` The API answers at once with `202` and the task ID; generation runs in the background. Response 202 (excerpt): ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "model": "dreamina-seedance-2-0-260128", "status": "queued", "settlement_status": "reserved", "quote": { "currency": "USD", "reserved_amount": "0.782453", "estimated_usage_upper_bound": { "video_tokens": 111779 } }, "created_at": 1793167380 } ``` > **Note**: Funds for the request's upper bound are reserved first (`quote.reserved_amount`). After completion the task is settled on actual usage and the rest is released; failed and cancelled tasks are free. ### 3. Get the result A task moves through the states below; succeeded, failed and cancelled are final. `queued` (Queued) → `processing` (Processing) → `succeeded` (Succeeded); the other final states `failed` (Failed) and `cancelled` (Cancelled) are free. #### Poll the task Read `GET /v1/video/tasks/{task_id}` every few seconds until `status` is final. ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` Response 200 (excerpt): ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "status": "succeeded", "output": { "assets": [ { "media_type": "video", "role": "result", "url": "https://media.example.invalid/download/result-0001?signature=example", "expires_at": 1793253785 } ] } } ``` After success, `output.assets[0].url` is the download link of the video. It works for up to 24 hours and never after `expires_at` (7 days after the result is ready by default); query the task again for a fresh link. It is null after `expires_at`. Save the video to your own storage: ```bash curl --output video.mp4 "$VIDEO_URL" ``` #### Receive a callback Instead of polling, let us notify you: add and verify an HTTPS address on the [Callbacks](./callbacks.html) page of the console, then put it in `callback_url` when you create a task. You receive `task.completed` when the task finishes and `task.billing_settled` once its charge is settled. [Verify the signature](./api-docs-en-callbacks-verify.md) before you handle an event. ```json "callback_url": "https://example.com/webhooks/chenvoya" ``` ### Next steps - [Generate a video](./api-docs-en-video-generate.md): Requests for text to video, first frames, first and last frames, reference images and reference media. - [How billing works](./api-docs-en-billing.md): Funds for the upper bound are reserved at creation and settled on actual usage; failed and cancelled tasks are free. - [Create a video task](./api-docs-en-api-video-tasks-create.md): Submit a prompt with optional images, videos or audio to create an asynchronous generation task; the task ID and the reserved amount come back at once. --- ## Authentication Send your API key in a request header with the Bearer scheme. Every request carries an API key in the `Authorization` header. The examples only reference the `CHENVOYA_API_KEY` environment variable and never show a key. ### Send the API key Write `Bearer`, a space and the API key in the `Authorization` header: ```bash Authorization: Bearer ${CHENVOYA_API_KEY} ``` A missing or invalid key returns `401 authentication_failed`, and the `WWW-Authenticate` response header names the Bearer scheme. ### Create and keep keys - Create API keys on the [API keys](./integration.html) page of the console; one API key works with every model. - Keep and use API keys on servers only, in environment variables or a secret manager. Never put one in a web page, an app, a code repository, a document or a log. - If you suspect a leak, suspend the key in the console, create a new one and replace the value on your servers. ### Permissions and teams API keys belong to a team, and tasks and assets are separated by team: a task created with any API key of your team can be read with any other key of the same team. Tasks and assets of other teams always return `task_not_found` or `asset_not_found`. Registering and reading assets needs an API key with `service.use` permission; otherwise the answer is `403 permission_denied`. ### Authentication errors | Status | error.code | What to do | | --- | --- | --- | | 401 | `authentication_failed` | Check the API key after `Authorization: Bearer`. | | 403 | `permission_denied` | Use an API key that has `service.use` permission. | | 409 | `key_provisioning_incomplete` | The key's spend limit setup did not finish: delete the key and create a new one. | ### Next steps - [Request conventions](./api-docs-en-conventions.md): Shared rules for the API address, JSON, request IDs, times, amounts and paging. - [Quickstart](./api-docs-en-quickstart.md): Generate your first video in three steps: create an API key, submit a task, get the result. - [Error codes](./api-docs-en-api-errors.md): Every error.code with its meaning, status and endpoints. --- ## Request conventions Shared rules for the API address, JSON, request IDs, times, amounts and paging. Every endpoint follows these conventions. The endpoint pages describe their own fields. ### API address Save the API address as `CHENVOYA_BASE_URL` without a trailing slash; every path in these docs follows it, for example `POST /v1/video/generations`. The address is shown in the [API overview](./api-docs-en-api.md#connection) and on the console's Integration page. ### JSON Request and response bodies are JSON; send `Content-Type: application/json` with a body. Field names and values are English identifiers: use them as they are. ### Request IDs You may send your own request ID in the `X-Request-ID` header; invalid or repeated values are replaced. The `X-Request-ID` response header returns the ID used for the request, at most 64 characters. When you report a problem, include it with the `trace_id` of the error response. ### IDs | ID | Format | Example | | --- | --- | --- | | Task ID | A letter, then 4–128 letters, digits, `_` or `-` in all | `task_32ae2a5177b310d3bc1ec83a98f63889` | | Asset ID | `ast-` and 32 lowercase hexadecimal digits | starts with `ast-` | | Request ID `request_id` | Same format as a task ID | `req_6acd96894df9798feb0ddb21e5a52d70` | | Billing correlation ID `billing_correlation_id` | Same format as a task ID | `billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0` | ### Times Time fields are whole seconds since the Unix epoch, such as `created_at`. A time that has not happened yet, such as `started_at` of a task that has not started, is `null`. ### Amounts Amounts are USD, written as strings with six decimal places, such as `"0.361564"`. An amount that is not final yet is `null`. Do not convert amounts to floating point before you calculate. ### Paging Lists page with a cursor: `page_size` sets the page length (1–100, default 50), and the `next_cursor` of a response goes into the `cursor` of the next request. The last page has a null `next_cursor`. Cursors are opaque: never build one yourself; a cursor the service did not issue for this list of your team returns `400 invalid_request`. ### Errors Error responses carry an `error` object and a `trace_id`. Handle `error.code`; see [Handle errors](./api-docs-en-errors.md). ### Next steps - [Authentication](./api-docs-en-authentication.md): Send your API key in a request header with the Bearer scheme. - [Idempotency and retries](./api-docs-en-idempotency.md): Retry with the same Idempotency-Key to get exactly one task and one charge. - [API overview](./api-docs-en-api.md): Base URL, authentication, errors and every endpoint. --- ## Video models What Seedance 2.0, 2.0 Fast and 2.0 Mini accept and produce. The three models share one API key and one set of request parameters; put the model ID in the `model` field of the body. ### Available models | Model | Model ID | Resolution | Duration (s) | | --- | --- | --- | --- | | Seedance 2.0 | `dreamina-seedance-2-0-260128` | 480p · 720p · 1080p | 4–15, default 5 | | Seedance 2.0 Fast | `dreamina-seedance-2-0-fast-260128` | 480p · 720p | 4–15, default 5 | | Seedance 2.0 Mini | `dreamina-seedance-2-0-mini-260615` | 480p · 720p | 4–15, default 5 | Seedance 2.0 Fast and Seedance 2.0 Mini accept only `480p` or `720p` as `resolution`. ### Inputs - A request has at most 16 `content` items: a text prompt, plus up to 9 images, 3 reference videos and 3 reference audio clips, with at most 15 materials in all. - A first-frame request takes one image and a first-and-last-frame request two, with at most one `first_frame` and one `last_frame`. - Each reference video or audio clip lasts 2–15 seconds; the videos together and the audio clips together each stay within 15 seconds. Reference audio needs a reference image or video. - Images and audio can be HTTPS links, `asset://` references or base64 data URIs; videos can be HTTPS links or `asset://` references. See [Generate a video](./api-docs-en-video-generate.md#sources). ### Outputs | Parameter | Values | Default | | --- | --- | --- | | `duration` | Whole seconds from 4 to 15 | `5` | | `resolution` | `480p` · `720p` · `1080p` | `720p` | | `ratio` | `adaptive` · `21:9` · `16:9` · `4:3` · `1:1` · `3:4` · `9:16` | `adaptive` | | `generate_audio` | `true` · `false` | `false` | | `watermark` | `true` · `false` | `false` | | `seed` | An integer from -1 to 4294967295; -1 means random | `-1` | | `return_last_frame` | `true` · `false` | `false` | ### Charges Usage is recorded in meters such as `video_tokens`: funds for the upper bound are reserved at creation and the task is settled on actual usage, as [How billing works](./api-docs-en-billing.md) explains. List prices are on the [Pricing](./public-prices.html) page; your team's prices are in the console. ### Next steps - [Generate a video](./api-docs-en-video-generate.md): Requests for text to video, first frames, first and last frames, reference images and reference media. - [How billing works](./api-docs-en-billing.md): Funds for the upper bound are reserved at creation and settled on actual usage; failed and cancelled tasks are free. - [Create a video task](./api-docs-en-api-video-tasks-create.md): Submit a prompt with optional images, videos or audio to create an asynchronous generation task; the task ID and the reserved amount come back at once. --- ## Generate a video Requests for text to video, first frames, first and last frames, reference images and reference media. Every case calls the same endpoint, `POST /v1/video/generations`; only the `content` items differ. The task, its reservation and its idempotency record are accepted together, and the API answers with `202` and the task ID while generation runs in the background. All parameters are on [Create a video task](./api-docs-en-api-video-tasks-create.md). ### Text to video Put a single `text` item in `content` that describes the scene, the action and the camera. ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "content": [ { "type": "text", "text": "A paper boat crossing a moonlit lake." } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### First frame and last frame Put each image in an `image_url` item with `role` set to `first_frame` or `last_frame`. With a single image you may leave out `role` and it becomes the first frame; with several images every one needs a `role`. ```json "content": [ {"type": "text", "text": "A paper boat crossing a moonlit lake."}, {"type": "image_url", "role": "first_frame", "image_url": {"url": "https://example.com/first.png"}}, {"type": "image_url", "role": "last_frame", "image_url": {"url": "https://example.com/last.png"}} ] ``` ### Reference images Set `role` to `reference_image`, with up to 9 images. This example references a registered asset: ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 ASSET_ID_PATTERN='^ast-[0-9a-f]{32}$' if [[ ! ${ASSET_ID} =~ ${ASSET_ID_PATTERN} || ${ASSET_ID} == *[!A-Za-z0-9._-]* ]]; then printf '%s\n' 'Set ASSET_ID to the ID from the creation response first.' >&2 exit 1 fi curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "duration": 5, "ratio": "16:9", "resolution": "720p", "content": [ { "type": "text", "text": "Animate the reviewed reference image." }, { "type": "image_url", "role": "reference_image", "image_url": { "url": "asset://'"${ASSET_ID}"'" } } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"duration\":5,\"ratio\":\"16:9\",\"resolution\":\"720p\",\"content\":[{\"type\":\"text\",\"text\":\"Animate the reviewed reference image.\"},{\"type\":\"image_url\",\"role\":\"reference_image\",\"image_url\":{\"url\":\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"}}]}" ASSET_ID = os.environ["ASSET_ID"] if not re.fullmatch("^ast-[0-9a-f]{32}$", ASSET_ID): sys.exit("Set ASSET_ID to the ID from the creation response") body_text = body_text.replace("\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"", json.dumps("asset://" + ASSET_ID)) headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"duration\":5,\"ratio\":\"16:9\",\"resolution\":\"720p\",\"content\":[{\"type\":\"text\",\"text\":\"Animate the reviewed reference image.\"},{\"type\":\"image_url\",\"role\":\"reference_image\",\"image_url\":{\"url\":\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"}}]}"; const ASSET_ID = process.env.ASSET_ID; if (typeof ASSET_ID !== "string" || (new RegExp("^ast-[0-9a-f]{32}$")).exec(ASSET_ID)?.[0] !== ASSET_ID) throw new Error("Set ASSET_ID to the ID from the creation response"); bodyText = bodyText.replaceAll("\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"", JSON.stringify("asset://" + ASSET_ID)); headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strconv" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"duration\":5,\"ratio\":\"16:9\",\"resolution\":\"720p\",\"content\":[{\"type\":\"text\",\"text\":\"Animate the reviewed reference image.\"},{\"type\":\"image_url\",\"role\":\"reference_image\",\"image_url\":{\"url\":\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"}}]}" assetID := os.Getenv("ASSET_ID") if !regexp.MustCompile(`\A(?:ast-[0-9a-f]{32})\z`).MatchString(assetID) { fail("Set ASSET_ID to the ID from the creation response") } bodyText = strings.ReplaceAll(bodyText, "\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"", strconv.Quote("asset://"+assetID)) request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### Reference video and audio - Reference videos use `video_url` items with `role` `reference_video`; reference audio uses `audio_url` items with `role` `reference_audio`. - Each clip lasts 2–15 seconds; the videos together and the audio clips together each stay within 15 seconds, with at most 3 videos and 3 audio clips. - Reference audio needs a reference image or video. - A reference video given as an HTTPS link reserves funds for 15 input seconds. Preparation checks the actual length: a task whose total is too long fails with `input_duration_exceeded`, the whole reservation is released and nothing is charged. ### Media sources Each image, video or audio item takes one of three sources: | Source | Form | Notes | | --- | --- | --- | | HTTPS link | `https://…` | The platform downloads, checks and stores the file before generation; meanwhile the task is in `preparing_assets`. If preparation fails, the whole reservation is released and nothing is charged. | | Registered asset | `asset://` and the asset ID | [Register the asset](./api-docs-en-assets.md) first and reference it once it is `active`. | | base64 data URI | `data:image/png;base64,…` | Images in JPEG, PNG or WebP, smaller than 30 MB once decoded; audio in WAV or MP3, up to 15 MB and 2–15 seconds; not for video. | HTTPS links use port 443 and carry no user information, IP address or local host name, at most 2048 bytes. One request may combine HTTPS links and base64 data, but neither can be mixed with `asset://` references. ### Sound, aspect ratio and duration - `generate_audio`: whether to generate sound with the video; default `false`. - `ratio`: aspect ratio; default `adaptive`, chosen by the model from the input. - `duration`: 4–15 seconds; default 5. - `resolution`: default `720p`; Seedance 2.0 Fast and Mini go up to `720p`. - `watermark`, `seed`, `return_last_frame`: see [Video models](./api-docs-en-video-models.md#outputs). ### Large requests A body can be up to 64 MiB. A body over 2 MiB must arrive within 120 seconds, and each team can send only a few large requests at the same time; when they are all in use the answer is `429 rate_limited`: retry later with the same `Idempotency-Key`. ### Next steps - [Task status and results](./api-docs-en-video-tasks.md): What the task states mean, polling or callbacks, downloading results and cancelling tasks. - [Upload assets](./api-docs-en-assets.md): Register an asset, wait until it is active, then reference it with asset:// in generation requests. - [Create a video task](./api-docs-en-api-video-tasks-create.md): Submit a prompt with optional images, videos or audio to create an asynchronous generation task; the task ID and the reserved amount come back at once. --- ## Task status and results What the task states mean, polling or callbacks, downloading results and cancelling tasks. After you create a task, read its progress and result by task ID, or wait for a callback. ### Task status `queued` (Queued) → `processing` (Processing) → `succeeded` (Succeeded); the other final states `failed` (Failed) and `cancelled` (Cancelled) are free. While a task is `processing`, `phase` names the current stage: preparing inputs, waiting, starting, generating, confirming the result, finishing and awaiting settlement. Every value is on [Statuses and reasons](./api-docs-en-api-statuses.md#task-phase). ### Three status fields | Field | Meaning | | --- | --- | | `status` | Task execution; the final states are `succeeded`, `failed` and `cancelled`. | | `delivery_status` | Delivery of the result files; download them once it is `available`. | | `settlement_status` | Billing settlement; the charge is final once it is `settled`. | ### Poll Read the task every few seconds until `status` is final: ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### Callbacks After you register and verify a callback address in the console, pass `callback_url` when you create a task and you receive `task.completed` when it finishes. The event has no download link: read the task once more. See [Receive callbacks](./api-docs-en-callbacks.md). ### Download the result A succeeded task lists its result files in `output.assets` with `role` `result`. `url` is an HTTPS download link; each task read signs a fresh one that works for up to 24 hours and never after `expires_at` (7 days after the result is ready by default); when it has expired or is not available, `url` is null and `download_status` says why. Save the file to your own storage in time. ```bash curl --output video.mp4 "$VIDEO_URL" ``` ### List tasks `GET /v1/video/tasks` lists all of your team's tasks, whichever API key created them, newest first. A list entry holds the IDs, model, statuses and times; read usage, charges, results and errors from the task by its ID. ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} path = "/v1/video/tasks" request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const path = "/v1/video/tasks"; const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/tasks" request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### Cancel a task `POST /v1/video/tasks/{task_id}/cancel` needs an `Idempotency-Key` and no body. - Before generation starts, cancellation takes effect at once: the answer is `effective`, the task becomes `cancelled` and the whole reservation is released. - After generation starts, the answer is `not_effective` with reason `generation_already_started`; the task continues and is settled as usual. - A finished task without a cancellation returns `409 task_not_cancellable`. ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/cancel" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/cancel", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("POST", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### Failure reasons A failed task gives its reason in `error.code`, such as `input_fetch_failed` when an input could not be downloaded or `generation_failed`. Every value is on [Statuses and reasons](./api-docs-en-api-statuses.md#task-error). Failed and cancelled tasks are settled at no charge. ### Next steps - [Receive callbacks](./api-docs-en-callbacks.md): After you register a callback address, we send it signed events when a task finishes and when its charge is settled. - [How billing works](./api-docs-en-billing.md): Funds for the upper bound are reserved at creation and settled on actual usage; failed and cancelled tasks are free. - [Read a video task](./api-docs-en-api-video-tasks-retrieve.md): Check execution progress, delivery, usage and the billing summary. --- ## Upload assets Register an asset, wait until it is active, then reference it with asset:// in generation requests. An asset is an image, video or audio file registered with the platform. Register it once and reference it in as many generation requests as you like. ### When to use assets An image, video or audio input of a generation request comes from one of three sources: an HTTPS link, a base64 data URI or a registered asset. Registering pays off when you use the same file again and again; for a single use, put the link in the request. The rules of each source are on [Generate a video](./api-docs-en-video-generate.md#sources). ### Register an asset Send the public HTTPS address, the media type and the model to `POST /v1/assets`. Registration needs an `Idempotency-Key`, and the API key needs `service.use` permission. ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/assets" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://images.example.test/reference.png", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference" }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/assets" body_text = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/assets"; let bodyText = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/assets" bodyText := "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` After acceptance the platform downloads and checks the file in the background; the `Location` response header is the path to read. ```json { "asset_id": "ast-11111111111111111111111111111111", "status": "processing" } ``` ### Read the status Read `GET /v1/assets/{asset_id}` until the status is no longer `processing`: | Status | Meaning | | --- | --- | | `processing` | Being downloaded and checked; it cannot be referenced yet. | | `active` | Ready to reference; size and digest are returned, with the dimensions of images and videos and the duration of audio and video. | | `unavailable` | Cannot be used; `failure_reason` says why. | ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/assets/${ASSET_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} ASSET_ID = os.environ["ASSET_ID"] if not re.fullmatch("^ast-[0-9a-f]{32}$", ASSET_ID): sys.exit("Set ASSET_ID to the ID from the creation response") path = "/v1/assets/{asset_id}".replace("{asset_id}", urllib.parse.quote(ASSET_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const ASSET_ID = process.env.ASSET_ID; if (!ASSET_ID) throw new Error("Set ASSET_ID"); if (typeof ASSET_ID !== "string" || (new RegExp("^ast-[0-9a-f]{32}$")).exec(ASSET_ID)?.[0] !== ASSET_ID) throw new Error("Set ASSET_ID to the ID from the creation response"); const path = "/v1/assets/{asset_id}".replace("{asset_id}", encodeURIComponent(ASSET_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } assetID := os.Getenv("ASSET_ID") if assetID == "" { fail("Set ASSET_ID") } if !regexp.MustCompile(`\A(?:ast-[0-9a-f]{32})\z`).MatchString(assetID) { fail("Set ASSET_ID to the ID from the creation response") } path := strings.Replace("/v1/assets/{asset_id}", "{asset_id}", url.PathEscape(assetID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### Reference an asset Write `asset://` and the asset ID in the `url` of an `image_url`, `video_url` or `audio_url` item. An asset registered for one model can also be used by requests for other compatible models. - Referencing an asset that is still processing returns `409 asset_not_ready`; once it is `active`, retry with the same `Idempotency-Key`. - An asset of another team, an unknown one and a deleted one all return `asset_not_found`. - One request cannot mix `asset://` references with HTTPS links or base64 data. ### Addresses and allowances - The source must be a public HTTPS address of at most 2048 bytes without user information; an unsafe destination returns `400 source_url_not_allowed`. - When your team's storage allowance is used up the answer is `429 asset_storage_quota_exceeded`; when intake capacity is limited it is `429 asset_intake_rate_limit_exceeded`: retry later. ### Next steps - [Generate a video](./api-docs-en-video-generate.md): Requests for text to video, first frames, first and last frames, reference images and reference media. - [Register an asset](./api-docs-en-api-assets-create.md): Requires service.use. - [The asset object](./api-docs-en-api-assets-object.md): For active assets, size_bytes and sha256 are required. --- ## Receive callbacks After you register a callback address, we send it signed events when a task finishes and when its charge is settled. Callbacks save you from polling: when a task finishes we send a `POST` request to the HTTPS address you registered. ### Register an address 1. On the [Callbacks](./callbacks.html) page of the console, add an HTTPS address and verify it. Keep the signing secret `signing_secret` shown at registration. 2. When you create a task, put `callback_url` in the body. It must match a verified, unexpired address of your team that has not been removed, byte for byte; otherwise the answer is `400 callback_target_not_verified`. 3. When an event arrives, [verify its signature](./api-docs-en-callbacks-verify.md) before you handle the body. ### Two events | Event | When it is sent | | --- | --- | | [`task.completed`](./api-docs-en-api-events-task-completed.md) | After the task succeeds, fails or is cancelled; a succeeded task finishes delivery first. | | [`task.billing_settled`](./api-docs-en-api-events-task-billing-settled.md) | After `task.completed` is delivered or abandoned and the charge is settled. Not sent for a task still unsettled thirty days after completion. | Both bodies are task snapshots without download links; read the task when you need to download. ### Your response and retries Only a 2xx response confirms receipt. Other answers and timeouts are retried: after the first failure we wait 10 seconds, 1 minute and 5 minutes, then try every hour, for up to 7 days per event. The 7 days of `task.billing_settled` start when it is first sent. ### Deduplicate Each event is delivered at least once and may arrive twice. After verifying the signature, deduplicate on `event_id` inside the body; the `Chenvoya-Event-Id` header is not signed and is only a convenience copy. Keep processed event IDs for at least 7 days and 5 minutes. Retries keep the event ID and the original body bytes. ### Remove an address After you remove an address in the console no new delivery starts, although an attempt already being sent may still arrive. The console shows delivery records; the current status and the download of a task always come from reading the task. ### Next steps - [Verify callback signatures](./api-docs-en-callbacks-verify.md): Use the signing secret from registration to check that every event comes from us. - [Receive a task completion notification](./api-docs-en-api-events-task-completed.md): Chenvoya sends task.completed when a task reaches succeeded, failed or cancelled. - [Receive a billing settlement notification](./api-docs-en-api-events-task-billing-settled.md): Chenvoya sends task.billing_settled after task.completed is delivered or abandoned and settlement reaches settled or settled_with_receivable. --- ## Verify callback signatures Use the signing secret from registration to check that every event comes from us. Every event carries a `Chenvoya-Signature` header. Verify the signature before you read the body; when verification fails, answer with a non-2xx status and drop the request. ### Signature format ```text Chenvoya-Signature: t=1793167400,v1=<64 lowercase hexadecimal digits> ``` `t` is the signing time in Unix seconds and `v1` is a lowercase hexadecimal HMAC-SHA256. ### Steps 1. Base64url-decode the `signing_secret` from registration to get the 32-byte verification key. 2. Split `t` and `v1` out of `Chenvoya-Signature`. 3. Reject `t` when it differs from your clock by more than 5 minutes. 4. Compute HMAC-SHA256 with the verification key over `chenvoya.v1.`, `t`, a dot `.` and the original body bytes. 5. Write the result as lowercase hexadecimal and compare it with `v1` in constant time. 6. Then deduplicate on `event_id` in the body, and reject events whose `created_at` is more than 7 days and 5 minutes old or more than 5 minutes in the future. ### Sample code ```python import base64 import hashlib import hmac import time def verify(signing_secret, header, body): fields = {} for item in header.split(","): name, _, value = item.strip().partition("=") fields[name] = value t, v1 = fields.get("t", ""), fields.get("v1", "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False key = base64.urlsafe_b64decode(signing_secret + "=" * (-len(signing_secret) % 4)) signed = b"chenvoya.v1." + t.encode() + b"." + body expected = hmac.new(key, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(signingSecret, header, body) { const fields = {}; for (const item of header.split(",")) { const at = item.indexOf("="); if (at > 0) fields[item.slice(0, at).trim()] = item.slice(at + 1); } const t = fields.t ?? "", v1 = fields.v1 ?? ""; if (!/^[0-9]+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const key = Buffer.from(signingSecret, "base64url"); const expected = createHmac("sha256", key).update("chenvoya.v1." + t + ".").update(body).digest("hex"); return v1.length === expected.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); } ``` ```go package callback import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "strconv" "strings" "time" ) // Verify reports whether body carries a valid Chenvoya-Signature header. func Verify(signingSecret, header string, body []byte) bool { fields := map[string]string{} for _, item := range strings.Split(header, ",") { name, value, _ := strings.Cut(strings.TrimSpace(item), "=") fields[name] = value } t, err := strconv.ParseInt(fields["t"], 10, 64) if err != nil || t < 0 || time.Since(time.Unix(t, 0)).Abs() > 5*time.Minute { return false } key, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(signingSecret, "=")) if err != nil { return false } mac := hmac.New(sha256.New, key) mac.Write([]byte("chenvoya.v1." + fields["t"] + ".")) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(fields["v1"])) } ``` ### Checklist - Sign the original bytes you received; do not parse and re-encode the JSON first. - Compare in constant time, never with plain string equality. - Keep your server clock accurate, or the 5-minute window rejects good events. - Deduplicate on `event_id` and keep processed event IDs for at least 7 days and 5 minutes. - Keep the signing secret on your servers, like an API key. ### Next steps - [Receive callbacks](./api-docs-en-callbacks.md): After you register a callback address, we send it signed events when a task finishes and when its charge is settled. - [Receive a task completion notification](./api-docs-en-api-events-task-completed.md): Chenvoya sends task.completed when a task reaches succeeded, failed or cancelled. --- ## How billing works Funds for the upper bound are reserved at creation and settled on actual usage; failed and cancelled tasks are free. Video generation is charged by usage, recorded in meters such as `video_tokens`. Every task goes through two steps: a reservation and a settlement. ### Reservation When you create a task, the platform takes the largest usage the request can produce (`quote.estimated_usage_upper_bound.video_tokens`) and the current price version, calculates `quote.reserved_amount` and reserves it from your balance. When the balance is not enough, the answer is `429 insufficient_balance` with the reservation needed in `error.details`, and no task is created. ### Settlement After the task finishes it is settled on actual usage (`usage.video_tokens`): the charge comes out of the reservation and the rest returns to your balance. Once `settlement_status` is `settled`, `billing.final_actual_cost` is the task's final charge. ### Failed and cancelled tasks are free - Once a failed or cancelled task is settled, its final charge and amount due are zero and the whole reservation is released. - When an HTTPS input cannot be prepared, the task fails with an `input_*` error, the whole reservation is released and nothing is charged for generation. - Refused requests (4xx) are refused before any reservation: nothing is reserved and nothing is charged. ### An example These numbers come from the example task of these docs: | Step | Usage | Amount (USD) | | --- | --- | --- | | Reserved at creation | Upper bound of 111779 `video_tokens` | `0.782453` | | Charged after completion | Actual 51652 `video_tokens` | `0.361564` | | Released | — | `0.420889` | ### Amounts Every amount is in USD, written as a string with six decimal places, such as `"0.361564"`. An amount that is not final yet is `null`. ### Prices List prices of each model are on the [Pricing](./public-prices.html) page; your team's prices and their versions are in the console. These docs never fix a price. ### Next steps - [Reconcile charges](./api-docs-en-billing-reconcile.md): Use task billing to check the reservation, debits, refunds and amount due of every task. - [Read task billing](./api-docs-en-api-video-tasks-billing.md): Retrieve the whole-task billing summary and one ordered page of permanent billing events. --- ## Reconcile charges Use task billing to check the reservation, debits, refunds and amount due of every task. Each task has a bill: a summary and a series of permanent billing events. The summary answers how much the task charged in total; the events show how the money moved. ### Read the bill ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/billing" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/billing", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` The summary covers the whole task and never turns into a subtotal of the event page. Page through events with `cursor` and `page_size`; the last page has a null `next_cursor`. ### Summary fields | Field | Meaning | | --- | --- | | `reserved_amount` | Amount reserved for the task. | | `charged_amount` | Charge recorded for the task. | | `funding_debit_amount` | Amount actually debited from your balance. | | `funding_refund_amount` | Amount actually returned to your balance. | | `outstanding_receivable` | Amount due that has not been collected. | | `final_actual_cost` | Final charge; `null` until settlement is final. | ### Billing events Every movement of money is an event, such as `hold_created` for a new reservation, `charge_captured` for a charge and `hold_released` for a released reservation. Creating, increasing, reducing or releasing a reservation by itself is not a debit, refund or change in the amount due; read the amount fields. Every event type is on [Statuses and reasons](./api-docs-en-api-statuses.md#billing-event). ### When billing needs review The usage of a few tasks needs review. Their `settlement_status` is `reconciliation_required`, `final_actual_cost` is `null`, and `reconciliation` gives the reason `reason_code`, the missing meters `missing_meters` and whether you need to act in `customer_action_required`. After the review the settlement becomes `settled` or `settled_with_receivable`. ### Correlation IDs `request_id` and `billing_correlation_id` stay the same across the task, its bill and its events, so you can match your orders with our bills. ### Next steps - [How billing works](./api-docs-en-billing.md): Funds for the upper bound are reserved at creation and settled on actual usage; failed and cancelled tasks are free. - [Read task billing](./api-docs-en-api-video-tasks-billing.md): Retrieve the whole-task billing summary and one ordered page of permanent billing events. - [Statuses and reasons](./api-docs-en-api-statuses.md): Every value of the task, delivery, settlement, asset and billing fields. --- ## Idempotency and retries Retry with the same Idempotency-Key to get exactly one task and one charge. Networks time out and responses get lost. An idempotency key lets you send again safely when you do not know whether the last attempt worked. ### Which requests need one Creating a video task, cancelling a video task and registering an asset must carry an `Idempotency-Key` header. ### Make a key - Generate a new value for every new operation, such as a UUID, and save it before you send. - Use 8–128 characters, starting with a letter or digit, with only letters, digits and `.`, `_`, `:` or `-`. - Keys are unique per team, operation and key, not per API key: two API keys of one team sending the same key name the same operation. ### When the outcome is unknown When a request times out, the connection drops, or you get `500` or `503`, resend the same body with the same saved key. An operation that already succeeded returns its original response; nothing is created or charged twice. Never generate a new value for a retry. ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} ``` All samples work like this: they reuse `IDEMPOTENCY_KEY` from the environment, or generate it once and ask you to save it. ### Conflicts | Status | error.code | Meaning | | --- | --- | --- | | 409 | `idempotency_conflict` | The key was already used for a different request. Retry the original request, or use a new key for a new request. | | 409 | `idempotency_replay_expired` | The saved replay of the original operation expired; the operation is not run again. Read the original object by its ID. | ### Cancellation keys A cancellation keeps its original response forever. Cancelling the same task again with a new key refers to the same cancellation and returns the task status saved at that time. ### Next steps - [Handle errors](./api-docs-en-errors.md): The shape of error responses, and which errors you can retry as they are. - [Rate limits and allowances](./api-docs-en-limits.md): Request rate, large-request concurrency, balance and asset allowances can each refuse a new request. --- ## Handle errors The shape of error responses, and which errors you can retry as they are. The HTTP status says how a request ended, and an error response carries a machine-readable code. Write your handling against the code, never against the message text. ### Error shape An error response has an `error` object and a `trace_id`: | Field | Meaning | | --- | --- | | `error.code` | Machine-readable code; handle errors by it. | | `error.message` | Readable explanation that may change; do not branch on it. | | `error.details` | Optional structured details, such as the reservation needed when the balance is low. | | `trace_id` | Trace ID for investigation; include it when you contact us. | ### Can you retry? | Case | What to do | | --- | --- | | `400`, `401`, `403`, `404` | The request itself is wrong and fails again as it is. Fix it by the code, then send it. | | `409` | Mostly an idempotency conflict, see [Idempotency and retries](./api-docs-en-idempotency.md#conflict); for `asset_not_ready`, retry with the same key once the asset is active. | | `429` | A rate, balance or allowance limit. With `Retry-After`, wait that many seconds and retry with the same key; top up first when the balance is low. | | `500`, `503` | The outcome is unknown. Retry the same body with the same `Idempotency-Key`; nothing is created or charged twice. | | Timeout or dropped connection | As above: retry as it is with the same key. | The meaning of every code, what to do and whether it is charged are on [Error codes](./api-docs-en-api-errors.md). ### Details - `unsupported_capability`: `error.details` lists the capabilities that cannot be met together and the request parameters involved; adjust those parameters. - `insufficient_balance`: `error.details` gives the reservation the request needs and its price version. ### Failed tasks A request the API accepted can still fail in the background. A failed task has `status` `failed` and a reason in `error.code`, such as `input_fetch_failed` or `generation_failed`; every value is on [Statuses and reasons](./api-docs-en-api-statuses.md#task-error). Failed tasks are settled at no charge. ### Next steps - [Error codes](./api-docs-en-api-errors.md): Every error.code with its meaning, status and endpoints. - [Idempotency and retries](./api-docs-en-idempotency.md): Retry with the same Idempotency-Key to get exactly one task and one charge. --- ## Rate limits and allowances Request rate, large-request concurrency, balance and asset allowances can each refuse a new request. Request rate, asset intake capacity, storage allowance, an API key's spend limit or your balance may refuse a new request. Handle limits by their error codes; these docs never fix the numbers. ### Rate limits Too many requests return `429 rate_limited`. The response may carry `Retry-After` (1–3600 seconds): wait that long, then retry with the same `Idempotency-Key`. ### Large-request concurrency Each team can send only a few creation requests with bodies over 2 MiB at the same time. When they are all in use the answer is `429 rate_limited` without `Retry-After`: retry a little later. A large body must arrive within 120 seconds. ### Balance and API key limits When your balance cannot cover the reservation the answer is `429 insufficient_balance`, with the reservation needed in `error.details`; top up and retry with the same key. When an API key has a spend limit, new requests are refused once it is used up as well: handle them by the returned code. ### Asset allowances - `429 asset_storage_quota_exceeded`: your team's asset storage allowance is used up; `Retry-After` does not apply. - `429 asset_intake_rate_limit_exceeded`: asset intake capacity is limited; retry later. ### Paused intake `503 funding_intake_closed` means new tasks are not accepted for now; exact replays of accepted requests still return their response. Try again later. ### Next steps - [Handle errors](./api-docs-en-errors.md): The shape of error responses, and which errors you can retry as they are. - [Idempotency and retries](./api-docs-en-idempotency.md): Retry with the same Idempotency-Key to get exactly one task and one charge. --- ## FAQ Common questions about charges, failures, how long results stay and API keys. If your question is not here, contact us with the `trace_id` or the task ID. ### Charges #### Are failed tasks charged? No. Once a failed or cancelled task is settled, its final charge and amount due are zero and the whole reservation is released. See [How billing works](./api-docs-en-billing.md#free). #### Why is the reservation larger than the final charge? At creation we reserve the largest usage the request can produce; after completion the task is settled on actual usage and the rest returns to your balance. ### Tasks #### My request timed out. Should I send it again? Yes, with the same body and the same `Idempotency-Key`. If the last attempt in fact succeeded, you get the original task back, and nothing is created or charged twice. See [Idempotency and retries](./api-docs-en-idempotency.md#retry). #### Can I cancel a task that is generating? Only before generation starts. After that, cancelling returns `not_effective` and the task continues and is settled as usual. #### How long can I download a result? The result is kept until `expires_at`, 7 days after it is ready by default. Each task read returns a fresh `url` that works for up to 24 hours and never after `expires_at`. Save the file to your own storage in time; after `expires_at` `url` is null. #### Why does a task return `task_not_found`? Tasks are separated by team. An unknown task ID and one of another team both return `404 task_not_found`. ### API keys and callbacks #### What if an API key leaks? Suspend it at once on the [API keys](./integration.html) page of the console, create a new one and replace the value on your servers. #### Why didn't a callback arrive? Check that the callback address is verified and not removed in the console, and that `callback_url` matches it byte for byte. Failed deliveries are retried for 7 days, and the console shows the delivery records. Whether or not a callback arrives, the task's status and result come from reading the task. ### Next steps - [Handle errors](./api-docs-en-errors.md): The shape of error responses, and which errors you can retry as they are. - [How billing works](./api-docs-en-billing.md): Funds for the upper bound are reserved at creation and settled on actual usage; failed and cancelled tasks are free. --- ## OpenAPI and Markdown Download the API definition, a page index for AI assistants and the Markdown of every page. The documentation also comes in machine-readable forms, for generating client code or handing to an AI assistant. ### API definition [api-openapi.json](./api-openapi.json) is the API definition in OpenAPI 3.1. It holds only the customer endpoints and the callback events and can be imported into API tools or code generators. ### llms.txt [api-llms.txt](./api-llms.txt) lists the Markdown address and a one-line summary of every English page, as an entry point for AI assistants. ### Markdown - Every page has Copy Markdown and View Markdown at the top, with the same content as the page. - The whole Chinese documentation in one file: [api-docs.md](./api-docs.md). - The whole English documentation in one file: [api-docs-en.md](./api-docs-en.md). ### Next steps - [Quickstart](./api-docs-en-quickstart.md): Generate your first video in three steps: create an API key, submit a task, get the result. - [API overview](./api-docs-en-api.md): Base URL, authentication, errors and every endpoint. --- ## API overview Base URL, authentication, errors and every endpoint. **API address**: Ask the person who opened your account for the API address. ### Authentication Customer calls use an API key with the Bearer scheme in a request header. Keep the value in the local CHENVOYA_API_KEY environment variable. Examples reference the variable only. Never paste its actual value into documentation or logs. ```bash Authorization: Bearer ${CHENVOYA_API_KEY} ``` ### Error responses Errors contain an error object and trace_id. Handle error.code in your application; message is a readable explanation and optional details carries structured information. Each operation below lists its error codes and states. The notes do not imply extra retry or billing guarantees. Every error code and what to do about it is on [Error codes](./api-docs-en-api-errors.md). ### All endpoints | Method | Path | Description | | --- | --- | --- | | POST | `/v1/video/generations` | [Create a video task](./api-docs-en-api-video-tasks-create.md) | | GET | `/v1/video/tasks/{task_id}` | [Read a video task](./api-docs-en-api-video-tasks-retrieve.md) | | GET | `/v1/video/tasks` | [List team tasks](./api-docs-en-api-video-tasks-list.md) | | POST | `/v1/video/tasks/{task_id}/cancel` | [Cancel a video task](./api-docs-en-api-video-tasks-cancel.md) | | GET | `/v1/video/tasks/{task_id}/billing` | [Read task billing](./api-docs-en-api-video-tasks-billing.md) | | POST | `/v1/assets` | [Register an asset](./api-docs-en-api-assets-create.md) | | GET | `/v1/assets/{asset_id}` | [Check an asset](./api-docs-en-api-assets-retrieve.md) | | Event | `task.completed` | [Receive a task completion notification](./api-docs-en-api-events-task-completed.md) | | Event | `task.billing_settled` | [Receive a billing settlement notification](./api-docs-en-api-events-task-billing-settled.md) | --- ## The video task object Detailed task view. With an unavailable result, the task stays processing and settlement remains under review. Succeeded tasks have result files and available delivery. Failed or cancelled tasks can have pending or settled billing; once settled, their final charge and amount due are zero. ### Attributes | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `id` | string | Required | Task ID assigned by the platform. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `object` | string | Required | Identifies the kind of object returned. Always: video.task. | | `request_id` | string | Required | Request ID used to correlate this task. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `billing_correlation_id` | string | Required | Billing correlation ID for this task. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `model` | string | Required | ID of the model used for this request. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `status` | string | Required | Current state; see the value notes for its meaning. `queued`: Queued; `processing`: Processing; `succeeded`: Succeeded; `failed`: Failed; `cancelled`: Cancelled; | | `phase` | string | Required, nullable | Current execution phase, or null when no phase applies. `preparing_assets`: Preparing inputs; `waiting_route`: Waiting; `submitting`: Starting; `provider_processing`: Generating; `reconciling`: Confirming the result; `finalizing`: Finishing; `settlement_pending`: Awaiting settlement; `null`: Null value; | | `delivery_status` | string | Required | Progress of result file delivery. `pending`: Not yet delivered; `materializing`: Preparing the download; `available`: Ready to download; `output_unavailable`: Result unavailable; | | `settlement_status` | string | Required | Current progress of billing settlement. `reserved`: Reserved; `pending`: Pending; `reconciliation_required`: Under review; `settled`: Settled; `settled_with_receivable`: Settled - amount due; | | `created_at` | integer | Required | Creation time as whole seconds since the Unix epoch. Range: ≥ 0. | | `started_at` | integer | Required, nullable | Time the task started, or null before it starts. Range: ≥ 0. | | `completed_at` | integer | Required, nullable | Time the task finished, or null while unfinished. Range: ≥ 0. | | `usage` | object | Required, nullable | Recorded usage, or null when it is not available. | | `usage.meter_type` | string | Required | The meter used to record usage. Always: seedance_video_generation. | | `usage.video_tokens` | integer | Required, nullable | Number of video tokens. The field type determines whether an actual usage value may be null. Range: ≥ 0. | | `usage.output_duration_seconds` | integer | Required | Duration of the output video in seconds. Range: ≥ 0. | | `usage.input_video_duration_seconds` | integer | Required | Combined duration of reference video input in seconds. Range: ≥ 0. | | `usage.resolution` | string | Required | Output resolution. The fast and mini models accept only 480p or 720p. `480p`: 480-line resolution; `720p`: 720-line resolution; `1080p`: 1080-line resolution; | | `usage.fps` | integer | Required | Frames per second in the video. Range: 1–120. | | `usage.has_reference_video` | boolean | Required | Whether reference video was used as input. | | `billing` | object | Required | Billing summary for the task, or null. | | `billing.currency` | string | Required | Currency of the amounts: USD. Always: USD. | | `billing.final_actual_cost` | string | Required, nullable | Final charge, or null while the amount is not final yet. Format: 6 decimal places. | | `billing.collected_liability_amount` | string | Required | Amount collected toward the charge. Format: 6 decimal places. | | `billing.funding_debit_amount` | string | Required | Amount debited from balance. Format: 6 decimal places. | | `billing.refunded_liability_amount` | string | Required | Amount of the charge refunded. Format: 6 decimal places. | | `billing.funding_refund_amount` | string | Required | Amount refunded to balance. Format: 6 decimal places. | | `billing.outstanding_receivable` | string | Required | Amount due that has not been collected. Format: 6 decimal places. | | `billing.billing_url` | string | Required | Relative path to this task's billing details. Format: /v1/video/tasks/{task_id}/billing. | | `output` | object | Required, nullable | Generated result information, or null. | | `output.assets` | array | Required | Files belonging to the generated result. Items: 1–16. | | `output.assets.asset_id` | string | Required | ID assigned to the asset by the platform. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `output.assets.media_type` | string | Required | The kind of media in this file. `video`: Video media; `image`: Image media; `audio`: Audio media; | | `output.assets.role` | string | Required | How the asset is used in the input or result. `result`: Result file; `last_frame`: Last frame; `audio`: Audio media; | | `output.assets.url` | string | Required, nullable | HTTPS download link when ready, or null when expired or unavailable. Length: 12–4096. Format: https://. | | `output.assets.materialized_at` | integer | Required, nullable | Time the result became ready, in whole Unix seconds. Range: ≥ 0. | | `output.assets.expires_at` | integer | Required, nullable | Expiry time of the result: the time it becomes ready plus the retention period in force at that time (7 days by default), measured separately from task completion. Range: ≥ 0. | | `output.assets.delivery_source` | string | Required | Where the result is made available for delivery. `platform`: Storage: Platform; | | `output.assets.materialization_status` | string | Required | Progress of preparing the result. `completed`: Saved; `retrying`: Saving; `failed`: Failed; | | `output.assets.download_status` | string | Required | Whether the result is ready to download, expired or unavailable. `available`: Available; `expired`: Expired; `unavailable`: Unavailable; | | `cancellation` | object | Required, nullable | Cancellation information already attached to the task, or null. | | `cancellation.cancel_operation_id` | string | Required | Stable ID of the cancellation operation. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `cancellation.status` | string | Required | Current state; see the value notes for its meaning. `requested`: Requested; `effective`: Cancelled; `not_effective`: Not cancelled; `failed`: Failed; | | `cancellation.reason_code` | string | Required, nullable | Reason cancellation did not take effect or failed. Null when requested or effective. Length: 1–96. Characters: a–z 0–9 _. | | `error` | object | Required, nullable | An object describing the error. | | `error.code` | string | Required | A code your application can use to handle the error. `no_common_asset_route`: Assets temporarily unavailable; `asset_not_ready`: A referenced asset is still processing; `asset_review_timeout`: Asset review timed out; `route_misconfigured`: Service configuration error; `route_temporarily_unavailable`: Temporarily unavailable; `generation_expired`: Generation expired; `generation_failed`: Generation failed; `input_fetch_failed`: An input could not be downloaded from its URL; `input_url_not_allowed`: An input URL is not allowed; `input_too_large`: An input exceeds its size limit; `input_unsupported`: An input format, dimensions or duration is unsupported; `input_rejected`: An input did not pass content review; `input_preparation_failed`: The inputs could not be prepared in time; `input_duration_exceeded`: The combined input duration exceeds the model limit; | | `error.message` | string | Required | A readable explanation of the error. Length: 1–1024. | | `error.details` | object | Optional | Optional fields with more information about the error. | | `error.details.content_index` | integer | Optional | Zero-based position of the failed input in the original content array, including text items. Range: 0–50. | ### Example ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "phase": null, "delivery_status": "available", "settlement_status": "settled", "created_at": 1793167380, "started_at": 1793167381, "completed_at": 1793167382, "usage": { "meter_type": "seedance_video_generation", "video_tokens": 51652, "output_duration_seconds": 5, "input_video_duration_seconds": 0, "resolution": "720p", "fps": 24, "has_reference_video": false }, "billing": { "currency": "USD", "final_actual_cost": "0.361564", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "refunded_liability_amount": "0.000000", "funding_refund_amount": "0.000000", "outstanding_receivable": "0.000000", "billing_url": "/v1/video/tasks/task_32ae2a5177b310d3bc1ec83a98f63889/billing" }, "output": { "assets": [ { "asset_id": "asset_4d00c4c64086b1e29724c1f2349f3708", "media_type": "video", "role": "result", "url": "https://media.example.invalid/download/result-0001?signature=example", "materialized_at": 1793167385, "expires_at": 1793253785, "delivery_source": "platform", "materialization_status": "completed", "download_status": "available" } ] }, "cancellation": null, "error": null } ``` Read this object with [Read a video task](./api-docs-en-api-video-tasks-retrieve.md). --- ## Create a video task `POST /v1/video/generations` Submit a prompt with optional images, videos or audio to create an asynchronous generation task; the task ID and the reserved amount come back at once. HTTP 202 means the task was accepted; then read its progress and result by task ID. Images and audio may also be sent inline as base64, and the create body is limited to 64 MiB. A body over 2 MiB has a 120-second read deadline, and each team can send at most two at a time; more returns 429 rate_limited. One image may omit role to act as first_frame; omitting role among several images, or conflicting roles, is refused. - Put your API key after `Authorization: Bearer` in the request header. - Send an `Idempotency-Key`: when the outcome is unknown, retry with the same key and body to create one task and be charged once. - Generation runs in the background: poll [Retrieve a video task](./api-docs-en-api-video-tasks-retrieve.md) or receive `task.completed` at your `callback_url`. - Funds are reserved for the upper bound at creation and settled on actual usage; failed and cancelled tasks are free. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Authorization` | string | Required | Bearer, a space and your API key. A missing or invalid key returns 401 authentication_failed. | | `Content-Type` | string | Required | The body is JSON: application/json. | | `X-Request-ID` | string | Optional | Your own request identifier. An invalid or repeated value is replaced; the value used comes back in the X-Request-ID response header. | | `Idempotency-Key` | string | Required | Generate a new value for every new request and save it; it starts with a letter or digit. When the outcome is unknown, retry with the same key and the original body to get the original response; the same key with a different body returns 409 idempotency_conflict. Keys belong to your team, not to one API key. Length: 8–128. Characters: A–Z a–z 0–9 . _ : -. | ### Request body · application/json · Required The body accepts only the fields below; any other field is rejected. | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `model` | string | Required | ID of the model used for this request. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `content` | array | Required | Platform limits: 9 images, 3 videos and 3 audio items, 15 materials and 16 content items. Each video or audio clip lasts 2 to 15 seconds; video and audio totals are each at most 15 seconds. The platform downloads, scans, stores and reviews HTTPS inputs before generation. Preparation failure releases the whole reservation at zero charge. Known preparation failures include their content_index. Items: 1–16. Each item matches exactly one of these shapes. | | `content[] (Text) type` | string | Required | Identifies the kind of input item. Always: text. | | `content[] (Text) text` | string | Required | Prompt describing the video to generate. Length: 1–10000. | | `content[] (Image) type` | string | Required | Identifies the kind of input item. Always: image_url. | | `content[] (Image) role` | string | Optional | How the asset is used in the input or result. `reference_image`: Reference image; `first_frame`: First frame; `last_frame`: Last frame; | | `content[] (Image) image_url` | object | Required | An HTTPS image source, an asset:// platform asset reference, or a JPEG/PNG/WebP base64 data URI. HTTPS and inline inputs may be combined; neither can be mixed with asset:// references. Exactly one of these shapes applies. | | `content[] (Image) image_url.url (HTTPS URL)` | string | Required | Public HTTPS URL with no userinfo, literal IP, local or platform hostname. Port must be omitted or 443; at most 2048 UTF-8 bytes. Length: 1–2048. Format: https://. | | `content[] (Image) image_url.url (Asset reference)` | string | Required | Use asset:// and the ID of an active asset owned by your team. Do not put its original HTTPS source address here. Length: 12–128. Format: asset://ast-…. | | `content[] (Image) image_url.url (Base64 image)` | string | Required | JPEG, PNG or WebP data URI with base64 encoding. The decoded image must be smaller than 30 MB and pass image header checks. Length: ≤ 40000023. Format: data:image/…;base64,…. | | `content[] (Video) type` | string | Required | Identifies the kind of input item. Always: video_url. | | `content[] (Video) role` | string | Required | How the asset is used in the input or result. Always: reference_video. | | `content[] (Video) video_url` | object | Required | An HTTPS video source or an asset:// platform video reference. Use one origin throughout the request. Exactly one of these shapes applies. | | `content[] (Video) video_url.url (HTTPS URL)` | string | Required | Public HTTPS URL with no userinfo, literal IP, local or platform hostname. Port must be omitted or 443; at most 2048 UTF-8 bytes. Length: 1–2048. Format: https://. | | `content[] (Video) video_url.url (Asset reference)` | string | Required | Use asset:// and the ID of an active asset owned by your team. Do not put its original HTTPS source address here. Length: 12–128. Format: asset://ast-…. | | `content[] (Audio) type` | string | Required | Identifies the kind of input item. Always: audio_url. | | `content[] (Audio) role` | string | Required | How the asset is used in the input or result. Always: reference_audio. | | `content[] (Audio) audio_url` | object | Required | An HTTPS audio source, an asset:// platform audio reference, or a WAV/MP3 base64 data URI (wav, x-wav, mpeg and mp3 aliases). HTTPS and inline inputs may be combined; neither can be mixed with asset:// references. Exactly one of these shapes applies. | | `content[] (Audio) audio_url.url (HTTPS URL)` | string | Required | Public HTTPS URL with no userinfo, literal IP, local or platform hostname. Port must be omitted or 443; at most 2048 UTF-8 bytes. Length: 1–2048. Format: https://. | | `content[] (Audio) audio_url.url (Asset reference)` | string | Required | Use asset:// and the ID of an active asset owned by your team. Do not put its original HTTPS source address here. Length: 12–128. Format: asset://ast-…. | | `content[] (Audio) audio_url.url (Base64 audio)` | string | Required | WAV or MP3 data URI with base64 encoding, up to 15 MB and 2 to 15 seconds. The audio/x-wav and audio/mp3 aliases are accepted. Length: ≤ 20000024. Format: data:audio/…;base64,…. | | `resolution` | string | Optional | Output resolution. Seedance 2.0 Fast and Mini accept only 480p or 720p. Default: 720p. `480p`: 480-line resolution; `720p`: 720-line resolution; `1080p`: 1080-line resolution; | | `ratio` | string | Optional | Aspect ratio of the output image. Default: adaptive. `adaptive`: Auto aspect ratio; `21:9`: Ultrawide landscape; `16:9`: Widescreen landscape; `4:3`: Landscape; `1:1`: Square; `3:4`: Portrait; `9:16`: Tall portrait; | | `duration` | integer | Optional | Output duration in whole seconds from 4 to 15; the default is 5. Default: 5. Range: 4–15. | | `generate_audio` | boolean | Optional | Whether to generate audio with the video. Default: false. | | `seed` | integer | Optional | Random seed; the default -1 means random. Range: -1–4294967295. Default: -1. | | `watermark` | boolean | Optional | Whether to add a watermark to the output. Default: false. | | `callback_url` | string | Optional | The address that receives the callback when the task ends. It must match, byte for byte, a callback address of your team that is verified in the console, not removed and not expired; otherwise 400 callback_target_not_verified. A retry with the same Idempotency-Key cannot add, drop or change it; that returns 409 idempotency_conflict. Length: 1–2048. Format: https://. | Not supported yet: `return_last_frame`: Setting it to `true` returns 400 `unsupported_capability`. `draft`, `output_format`, `omni_reference_task_type`, `execution_expires_after`, `priority`, `safety_identifier`: The current models do not take these; sending one returns 400 `invalid_request`. ### Returns · HTTP 202 Accepted: the task is created and the funds are reserved. The Location response header is the task's lookup path. | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `id` | string | Required | Task ID assigned by the platform. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `object` | string | Required | Identifies the kind of object returned. Always: video.task. | | `request_id` | string | Required | Request ID used to correlate this task. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `billing_correlation_id` | string | Required | Billing correlation ID for this task. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `model` | string | Required | ID of the model used for this request. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `status` | string | Required | Current state; see the value notes for its meaning. Always: queued. | | `phase` | string | Optional | The inputs are being prepared before generation starts. Always: preparing_assets. | | `settlement_status` | string | Required | Current progress of billing settlement. Always: reserved. | | `quote` | object | Required | Reservation quote recorded when the task is accepted. | | `quote.currency` | string | Required | Currency of the amounts: USD. Always: USD. | | `quote.reserved_amount` | string | Required | Amount that must be reserved to accept the task. Format: 6 decimal places. | | `quote.meter_type` | string | Required | The meter used to record usage. Always: seedance_video_generation. | | `quote.estimated_usage_upper_bound` | object | Required | Upper estimate of usage for the reservation. | | `quote.estimated_usage_upper_bound.video_tokens` | integer | Required | Number of video tokens. The field type determines whether an actual usage value may be null. Range: ≥ 1. | | `quote.price_version` | string | Required | Version of pricing applied to this quote. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `quote.quote_policy_version` | string | Required | Version of the policy used to calculate the reservation quote. Length: 1–128. Characters: A–Z a–z 0–9 . _ : -. | | `created_at` | integer | Required | Creation time as whole seconds since the Unix epoch. Range: ≥ 0. | #### Response headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `X-Request-ID` | string | Required | The identifier of this request: the valid value you sent, or its replacement. | | `Location` | string | Required | Canonical relative lookup path for the accepted object. | ### Status codes Each `error.code` and what to do about it is on [Error codes](./api-docs-en-api-errors.md). | Status | Meaning | error.code | | --- | --- | --- | | 202 | Accepted: the task is created and the funds are reserved. Response headers: `Location` | | | 400 | The format or parameters are wrong, or a referenced asset or callback address cannot be used. | asset_not_found, asset_unusable, callback_target_not_verified, input_video_duration_exceeded, invalid_request, unsupported_capability | | 401 | The API key is missing or invalid. Response headers: `WWW-Authenticate` | authentication_failed | | 409 | The idempotency key conflicts or its replay has expired, the API key is not ready, or an asset is still processing. | asset_not_ready, idempotency_conflict, idempotency_replay_expired, key_provisioning_incomplete | | 429 | The balance is too low, requests come too fast, or asset storage is full. Response headers: `Retry-After` | asset_storage_quota_exceeded, insufficient_balance, rate_limited | | 500 | Internal error. | internal_error | | 503 | The service is temporarily unavailable, or new tasks are paused. | dependency_unavailable, funding_intake_closed | ### Related guides - [Quickstart](./api-docs-en-quickstart.md) - [Generate a video](./api-docs-en-video-generate.md) - [Idempotency and retries](./api-docs-en-idempotency.md) - [Handle errors](./api-docs-en-errors.md) ### Request examples ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "content": [ { "type": "text", "text": "A paper boat crossing a moonlit lake." } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### Response examples 202 ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "queued", "settlement_status": "reserved", "quote": { "currency": "USD", "reserved_amount": "0.782453", "meter_type": "seedance_video_generation", "estimated_usage_upper_bound": { "video_tokens": 111779 }, "price_version": "price_06c9ad4cf0af577624218747c25fdb37", "quote_policy_version": "seedance2_quote_v1" }, "created_at": 1793167380 } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 409 ```json { "error": { "code": "idempotency_conflict", "message": "the idempotency key was used with a different canonical request" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "insufficient_balance", "message": "available USD balance is below the required reservation", "details": { "currency": "USD", "reserved_amount": "0.782453", "meter_type": "seedance_video_generation", "estimated_usage_upper_bound": { "video_tokens": 111779 }, "price_version": "price_06c9ad4cf0af577624218747c25fdb37" } }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## Read a video task `GET /v1/video/tasks/{task_id}` Check execution progress, delivery, usage and the billing summary. Task status, delivery status and settlement status describe separate parts of the work. A download link is null once it expires or while the file is unavailable. - Put your API key after `Authorization: Bearer` in the request header. - Returns the task status, delivery, usage and settlement summary; keep reading until `status` is final. - After success, download from `output.assets[].url`; the link is null when expired or unavailable. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Authorization` | string | Required | Bearer, a space and your API key. A missing or invalid key returns 401 authentication_failed. | | `X-Request-ID` | string | Optional | Your own request identifier. An invalid or repeated value is replaced; the value used comes back in the X-Request-ID response header. | ### Path parameters | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `task_id` | string | Required | ID of the video task to retrieve | ### Returns · HTTP 200 The current task, including delivery, usage and billing information. Returns [The video task object](./api-docs-en-api-video-tasks-object.md); its fields are described there. #### Response headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `X-Request-ID` | string | Required | The identifier of this request: the valid value you sent, or its replacement. | ### Status codes Each `error.code` and what to do about it is on [Error codes](./api-docs-en-api-errors.md). | Status | Meaning | error.code | | --- | --- | --- | | 200 | The current task, including delivery, usage and billing information. | | | 400 | The submitted request is not valid. | invalid_request | | 401 | The API key is missing or invalid. Response headers: `WWW-Authenticate` | authentication_failed | | 404 | No task with this ID is visible to your team. | task_not_found | | 429 | The request rate limit blocks this read operation. Response headers: `Retry-After` | rate_limited | | 500 | Internal error. | internal_error | | 503 | The service is temporarily unavailable. | dependency_unavailable | ### Related guides - [Task status and results](./api-docs-en-video-tasks.md) - [Quickstart](./api-docs-en-quickstart.md) - [Receive callbacks](./api-docs-en-callbacks.md) ### Request examples ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### Response examples 200 ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "phase": null, "delivery_status": "available", "settlement_status": "settled", "created_at": 1793167380, "started_at": 1793167381, "completed_at": 1793167382, "usage": { "meter_type": "seedance_video_generation", "video_tokens": 51652, "output_duration_seconds": 5, "input_video_duration_seconds": 0, "resolution": "720p", "fps": 24, "has_reference_video": false }, "billing": { "currency": "USD", "final_actual_cost": "0.361564", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "refunded_liability_amount": "0.000000", "funding_refund_amount": "0.000000", "outstanding_receivable": "0.000000", "billing_url": "/v1/video/tasks/task_32ae2a5177b310d3bc1ec83a98f63889/billing" }, "output": { "assets": [ { "asset_id": "asset_4d00c4c64086b1e29724c1f2349f3708", "media_type": "video", "role": "result", "url": "https://media.example.invalid/download/result-0001?signature=example", "materialized_at": 1793167385, "expires_at": 1793253785, "delivery_source": "platform", "materialization_status": "completed", "download_status": "available" } ] }, "cancellation": null, "error": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 404 ```json { "error": { "code": "task_not_found", "message": "task was not found" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## List team tasks `GET /v1/video/tasks` Returns tasks across your team, including those created with other API keys. Newer tasks come first, with Task ID breaking ties. Teams with no tasks receive an empty list. List items contain IDs, model, status, timestamps and cancellation details when a cancellation exists. Fetch task details for usage, billing, result files and errors. - Put your API key after `Authorization: Bearer` in the request header. - Lists all of your team's tasks, whichever API key created them, newest first. - Page with `next_cursor`; it is null on the last page. - Read usage, charges, results and errors from the task by its ID. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Authorization` | string | Required | Bearer, a space and your API key. A missing or invalid key returns 401 authentication_failed. | | `X-Request-ID` | string | Optional | Your own request identifier. An invalid or repeated value is replaced; the value used comes back in the X-Request-ID response header. | ### Query parameters | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `cursor` | string | Optional | Use next_cursor from the preceding page. A value not issued for this team's task list returns invalid_request. Length: 1–256. | | `page_size` | integer | Optional | Maximum number of tasks to return on this page. Default: 50. Range: 1–100. | ### Returns · HTTP 200 One page of the current team's tasks. | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `object` | string | Required | Identifies the kind of object returned. Always: list. | | `data` | array | Required | Task entries included in this page. Items: ≤ 100. | | `data.id` | string | Required | ID of the task in this list entry. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.request_id` | string | Required | Request ID used to correlate this task. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.billing_correlation_id` | string | Required | Billing correlation ID for this task. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.model` | string | Required | ID of the model used for this request. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `data.status` | string | Required | Current state; see the value notes for its meaning. `queued`: Queued; `processing`: Processing; `succeeded`: Succeeded; `failed`: Failed; `cancelled`: Cancelled; | | `data.phase` | string | Required, nullable | Current execution phase, or null when no phase applies. `preparing_assets`: Preparing inputs; `waiting_route`: Waiting; `submitting`: Starting; `provider_processing`: Generating; `reconciling`: Confirming the result; `finalizing`: Finishing; `settlement_pending`: Awaiting settlement; `null`: Null value; | | `data.delivery_status` | string | Required | Progress of result file delivery. `pending`: Not yet delivered; `materializing`: Preparing the download; `available`: Ready to download; `output_unavailable`: Result unavailable; | | `data.settlement_status` | string | Required | Current progress of billing settlement. `reserved`: Reserved; `pending`: Pending; `reconciliation_required`: Under review; `settled`: Settled; `settled_with_receivable`: Settled - amount due; | | `data.created_at` | integer | Required | Creation time as whole seconds since the Unix epoch. Range: ≥ 0. | | `data.started_at` | integer | Required, nullable | Time the task started, or null before it starts. Range: ≥ 0. | | `data.completed_at` | integer | Required, nullable | Time the task finished, or null while unfinished. Range: ≥ 0. | | `data.cancellation` | object | Optional, nullable | Stable Cancellation ID, status and reason code when a cancellation operation exists. Omitted otherwise. | | `data.cancellation.cancel_operation_id` | string | Required | Stable ID of the cancellation operation. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.cancellation.status` | string | Required | Current state; see the value notes for its meaning. `requested`: Requested; `effective`: Cancelled; `not_effective`: Not cancelled; `failed`: Failed; | | `data.cancellation.reason_code` | string | Required, nullable | Reason cancellation did not take effect or failed. Null when requested or effective. Length: 1–96. Characters: a–z 0–9 _. | | `next_cursor` | string | Required, nullable | Value for requesting the next page, or null on the last page. Length: 1–256. | #### Response headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `X-Request-ID` | string | Required | The identifier of this request: the valid value you sent, or its replacement. | ### Status codes Each `error.code` and what to do about it is on [Error codes](./api-docs-en-api-errors.md). | Status | Meaning | error.code | | --- | --- | --- | | 200 | One page of the current team's tasks. | | | 400 | The submitted request is not valid. | invalid_request | | 401 | The API key is missing or invalid. Response headers: `WWW-Authenticate` | authentication_failed | | 429 | The request rate limit blocks this read operation. Response headers: `Retry-After` | rate_limited | | 500 | Internal error. | internal_error | | 503 | The service is temporarily unavailable. | dependency_unavailable | ### Related guides - [Task status and results](./api-docs-en-video-tasks.md) - [Request conventions](./api-docs-en-conventions.md) ### Request examples ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} path = "/v1/video/tasks" request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const path = "/v1/video/tasks"; const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/tasks" request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### Response examples 200 ```json { "object": "list", "data": [ { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "phase": null, "delivery_status": "available", "settlement_status": "settled", "created_at": 1793167380, "started_at": 1793167381, "completed_at": 1793167382 } ], "next_cursor": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## Cancel a video task `POST /v1/video/tasks/{task_id}/cancel` Requires service.use permission and Idempotency-Key. Send an empty request body. Before generation starts, cancellation takes effect immediately: the task becomes cancelled and the full reserved amount is released. If billing is paused, the response still says effective and the release completes later. Once generation has started, cancellation returns not_effective; the task continues and billing proceeds normally. Retrying the same request with the same key replays the original response. Using that key for another task returns idempotency_conflict. A new key refers to the same cancellation operation and saves the task status at that response. A finished task with no cancellation operation returns task_not_cancellable. Unknown tasks and tasks outside your team return task_not_found, as with a task query. A rejected request does not reserve a new key. - Put your API key after `Authorization: Bearer`; send no request body. - Send an `Idempotency-Key`; the same key replays the original response. - Before generation starts, cancellation takes effect at once: the task becomes `cancelled` and the whole reservation is released. - After generation starts, the answer is `not_effective`; the task continues and is settled as usual. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Authorization` | string | Required | Bearer, a space and your API key. A missing or invalid key returns 401 authentication_failed. | | `X-Request-ID` | string | Optional | Your own request identifier. An invalid or repeated value is replaced; the value used comes back in the X-Request-ID response header. | | `Idempotency-Key` | string | Required | Generate a new value for every new request and save it; it starts with a letter or digit. When the outcome is unknown, retry with the same key and the original body to get the original response; the same key with a different body returns 409 idempotency_conflict. Keys belong to your team, not to one API key. Length: 8–128. Characters: A–Z a–z 0–9 . _ : -. | ### Path parameters | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `task_id` | string | Required | ID of the video task to retrieve | ### Returns · HTTP 202 A permanent snapshot of the cancellation operation. effective means Cancelled; not_effective means Not cancelled. Continue checking the task and its billing. | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `task_id` | string | Required | Video Task ID belonging to your team. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `cancel_operation_id` | string | Required | Stable Cancellation ID. New idempotency keys for the same task refer to this same ID. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `cancel_status` | string | Required | Cancellation status. Reports Cancelled or Not cancelled immediately. `requested`: Requested; `effective`: Cancelled; `not_effective`: Not cancelled; `failed`: Failed; | | `task_status_at_response` | string | Required | Task status saved when this idempotency key first receives a response. Replays keep this saved status. `queued`: Queued; `processing`: Processing; `succeeded`: Succeeded; `failed`: Failed; `cancelled`: Cancelled; | | `reason_code` | string | Required, nullable | Null when cancellation takes effect; generation_already_started when generation has already started and the task is not cancelled. Length: 1–96. Characters: a–z 0–9 _. | #### Response headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `X-Request-ID` | string | Required | The identifier of this request: the valid value you sent, or its replacement. | | `Location` | string | Required | Canonical relative lookup path for the accepted object. | ### Status codes Each `error.code` and what to do about it is on [Error codes](./api-docs-en-api-errors.md). | Status | Meaning | error.code | | --- | --- | --- | | 202 | A permanent snapshot of the cancellation operation. Response headers: `Location` | | | 400 | The submitted request is not valid. | invalid_request | | 401 | The API key is missing or invalid. Response headers: `WWW-Authenticate` | authentication_failed | | 404 | No task with this ID is visible to your team. | task_not_found | | 409 | The idempotency key belongs to a different request, or the finished task has no cancellation operation to refer to. | idempotency_conflict, idempotency_replay_expired, task_not_cancellable | | 429 | The request rate limit blocks this read operation. Response headers: `Retry-After` | rate_limited | | 500 | Internal error. | internal_error | | 503 | The service is temporarily unavailable. | dependency_unavailable | ### Related guides - [Task status and results](./api-docs-en-video-tasks.md) - [Idempotency and retries](./api-docs-en-idempotency.md) - [How billing works](./api-docs-en-billing.md) ### Request examples ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/cancel" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/cancel", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("POST", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### Response examples 202 ```json { "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "cancel_operation_id": "cancelop_ab99971fbec36056096527b7494b2059", "cancel_status": "requested", "task_status_at_response": "processing", "reason_code": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 404 ```json { "error": { "code": "task_not_found", "message": "task was not found" }, "trace_id": "trace_error_example" } ``` 409 ```json { "error": { "code": "task_not_cancellable", "message": "the terminal task has no cancellation operation" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## Read task billing `GET /v1/video/tasks/{task_id}/billing` Retrieve the whole-task billing summary and one ordered page of permanent billing events. Use cursor and page_size to continue. The summary always covers the task, rather than only the current event page. An unknown task or one outside your team returns task_not_found. - Put your API key after `Authorization: Bearer` in the request header. - The summary covers the whole task, never a subtotal of the current page. - Page through permanent billing events with `cursor` and `page_size`. - Amounts are USD strings with six decimal places. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Authorization` | string | Required | Bearer, a space and your API key. A missing or invalid key returns 401 authentication_failed. | | `X-Request-ID` | string | Optional | Your own request identifier. An invalid or repeated value is replaced; the value used comes back in the X-Request-ID response header. | ### Path parameters | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `task_id` | string | Required | ID of the video task to retrieve | ### Query parameters | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `cursor` | string | Optional | Opaque value for fetching the next page of permanent billing events. Length: 1–256. | | `page_size` | integer | Optional | Maximum number of billing events in this page. Default: 50. Range: 1–100. | ### Returns · HTTP 200 A whole-task billing summary with one ordered page of permanent events. | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `object` | string | Required | Identifies this object as task billing. Always: video.task.billing. | | `task_id` | string | Required | ID of the task owned by your team. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `request_id` | string | Required | Request ID associated with the task. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `billing_correlation_id` | string | Required | ID used to correlate this task's billing. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `model` | string | Required | Model used for this task. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `task_status` | string | Required | Progress of task execution. `queued`: Queued; `processing`: Processing; `succeeded`: Succeeded; `failed`: Failed; `cancelled`: Cancelled; | | `settlement_status` | string | Required | Progress of billing settlement for this task. `reserved`: Reserved; `pending`: Pending; `reconciliation_required`: Under review; `settled`: Settled; `settled_with_receivable`: Settled - amount due; | | `currency` | string | Required | Currency of the amounts: USD. Always: USD. | | `reserved_amount` | string | Required | Amount reserved for the task. Format: 6 decimal places. | | `charged_amount` | string | Required | Charge recorded for the task. Format: 6 decimal places. | | `collected_liability_amount` | string | Required | Amount collected toward the charge. Format: 6 decimal places. | | `funding_debit_amount` | string | Required | Amount debited from balance. Format: 6 decimal places. | | `refunded_liability_amount` | string | Required | Amount of the charge refunded. Format: 6 decimal places. | | `funding_refund_amount` | string | Required | Amount refunded to balance. Format: 6 decimal places. | | `outstanding_receivable` | string | Required | Amount due that has not been collected. Zero for settled tasks that failed or were cancelled. Format: 6 decimal places. | | `final_actual_cost` | string | Required, nullable | Final charge. Null while queued or processing, or while settlement is reserved, pending or reconciliation_required. An amount is provided for settled or settled_with_receivable. Settled tasks that failed or were cancelled have a zero final charge. Format: 6 decimal places. | | `events` | array | Required | Permanent billing events in this page. Items: ≤ 100. | | `events.billing_event_id` | string | Required | ID of this billing event. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `events.task_id` | string | Required | Task to which this event belongs. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `events.request_id` | string | Required | Request ID associated with the event. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `events.billing_correlation_id` | string | Required | ID used to correlate billing records. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `events.event_type` | string | Required | Kind of billing event recorded. `hold_created`: Reserved; `hold_increased`: Reservation increased; `hold_reduced`: Reservation reduced; `charge_captured`: Charged; `supplemental_charge`: Additional charge; `receivable_created`: Amount due recorded; `receivable_payment`: Amount due paid; `receivable_reduced`: Amount due reduced; `hold_released`: Reservation released; `refund_posted`: Refunded; `charge_waived`: Charge waived; `manual_adjustment`: Manual adjustment; | | `events.billing_component` | string | Required | Part of the service being billed. `generation_output`: Video generation; `reference_video`: Reference video; `reference_image`: Reference image; `reference_audio`: Reference audio; `web_search`: Web search; `other`: Other billing component; | | `events.amount` | string | Required | Amount of this event. Zero for charge_waived and greater than zero for hold_reduced. Format: 6 decimal places. | | `events.charge_amount` | string | Required | Charge amount recorded by this event. Format: 6 decimal places. | | `events.refund_amount` | string | Required | Refund amount recorded by this event. Format: 6 decimal places. | | `events.collected_liability_amount` | string | Required | Amount collected in this event. Format: 6 decimal places. | | `events.funding_debit_amount` | string | Required | Amount this event debited from balance. Format: 6 decimal places. | | `events.funding_refund_amount` | string | Required | Amount this event refunded to balance. Format: 6 decimal places. | | `events.receivable_amount` | string | Required | Change in the amount due recorded by this event. Format: 6 decimal places. | | `events.receivable_direction` | string | Required, nullable | increase for receivable_created; decrease for receivable_payment or receivable_reduced. Null for other event types. `increase`: Increase in amount due; `decrease`: Decrease in amount due; `null`: Null value; | | `events.adjustment_direction` | string | Required, nullable | debit or credit for manual_adjustment; null otherwise. `debit`: Debit direction; `credit`: Credit direction; `null`: Null value; | | `events.audit_reason` | string | Required, nullable | Audit explanation, present for manual_adjustment and null for other events. Length: 1–1024. | | `events.component_detail_code` | string | Required, nullable | Required detail code when billing_component is other; null for other components. Length: 1–96. Characters: a–z 0–9 _. | | `events.currency` | string | Required | Currency of the amounts: USD. Always: USD. | | `events.settlement_status` | string | Required | Settlement state recorded by this event. `reserved`: Reserved; `pending`: Pending; `reconciliation_required`: Under review; `settled`: Settled; `settled_with_receivable`: Settled - amount due; | | `events.created_at` | integer | Required | Time the event was created, in whole Unix seconds. Range: ≥ 0. | | `events.billing_operation_id` | string | Optional | ID of the billing operation, required for hold_reduced. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `events.quote_revision_before` | integer | Optional | Quote revision before the operation, required for hold_reduced. Range: ≥ 0. | | `events.quote_revision_after` | integer | Optional | Quote revision after the operation, required for hold_reduced. Range: ≥ 1. | | `events.reservation_revision_before` | integer | Optional | Reservation revision before the operation, required for hold_reduced. Range: ≥ 0. | | `events.reservation_revision_after` | integer | Optional | Reservation revision after the operation, required for hold_reduced. Range: ≥ 1. | | `next_cursor` | string | Required, nullable | Value for continuing to the next page, or null. Length: 1–256. | | `reconciliation` | object | Required, nullable | Review summary when settlement is reconciliation_required. Null for reserved, pending, settled and settled_with_receivable. | | `reconciliation.case_id` | string | Required | ID of the review case. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `reconciliation.reason_code` | string | Required | Code explaining why billing needs review. `missing_billable_usage`: Billable usage is missing; `usage_schema_drift`: The recorded usage shape has changed; `unknown_usage_unit`: The usage unit is not recognized; `price_rule_not_unique`: A single applicable price rule could not be determined; `output_unavailable`: Result unavailable; | | `reconciliation.missing_meters` | array | Required | Missing usage measurements, without duplicates. At least one is listed for missing_billable_usage. Items: ≤ 16. | | `reconciliation.customer_action_required` | boolean | Required | Whether you need to take any action. | #### Response headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `X-Request-ID` | string | Required | The identifier of this request: the valid value you sent, or its replacement. | ### Status codes Each `error.code` and what to do about it is on [Error codes](./api-docs-en-api-errors.md). | Status | Meaning | error.code | | --- | --- | --- | | 200 | A whole-task billing summary with one ordered page of permanent events. | | | 400 | The submitted request is not valid. | invalid_request | | 401 | The API key is missing or invalid. Response headers: `WWW-Authenticate` | authentication_failed | | 404 | No task with this ID is visible to your team. | task_not_found | | 429 | The request rate limit blocks this read operation. Response headers: `Retry-After` | rate_limited | | 500 | Internal error. | internal_error | | 503 | The service is temporarily unavailable. | dependency_unavailable | ### Related guides - [Reconcile charges](./api-docs-en-billing-reconcile.md) - [How billing works](./api-docs-en-billing.md) ### Request examples ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/billing" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/billing", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### Response examples 200 ```json { "object": "video.task.billing", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "task_status": "succeeded", "settlement_status": "settled", "currency": "USD", "reserved_amount": "0.782453", "charged_amount": "0.361564", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "refunded_liability_amount": "0.000000", "funding_refund_amount": "0.000000", "outstanding_receivable": "0.000000", "final_actual_cost": "0.361564", "events": [ { "billing_event_id": "bevt_example_hold_0001", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "event_type": "hold_created", "billing_component": "generation_output", "amount": "0.782453", "charge_amount": "0.000000", "refund_amount": "0.000000", "collected_liability_amount": "0.000000", "funding_debit_amount": "0.000000", "funding_refund_amount": "0.000000", "receivable_amount": "0.000000", "receivable_direction": null, "adjustment_direction": null, "audit_reason": null, "component_detail_code": null, "currency": "USD", "settlement_status": "reserved", "created_at": 1793167380 }, { "billing_event_id": "bevt_example_capture_0001", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "event_type": "charge_captured", "billing_component": "generation_output", "amount": "0.361564", "charge_amount": "0.361564", "refund_amount": "0.000000", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "funding_refund_amount": "0.000000", "receivable_amount": "0.000000", "receivable_direction": null, "adjustment_direction": null, "audit_reason": null, "component_detail_code": null, "currency": "USD", "settlement_status": "settled", "created_at": 1793167382 }, { "billing_event_id": "bevt_example_release_0001", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "event_type": "hold_released", "billing_component": "generation_output", "amount": "0.420889", "charge_amount": "0.000000", "refund_amount": "0.000000", "collected_liability_amount": "0.000000", "funding_debit_amount": "0.000000", "funding_refund_amount": "0.000000", "receivable_amount": "0.000000", "receivable_direction": null, "adjustment_direction": null, "audit_reason": null, "component_detail_code": null, "currency": "USD", "settlement_status": "settled", "created_at": 1793167383 } ], "next_cursor": null, "reconciliation": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 404 ```json { "error": { "code": "task_not_found", "message": "task was not found" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## The asset object For active assets, size_bytes and sha256 are required. Images and videos also include width and height; audio and video include duration_ms. Images omit duration_ms; audio omits dimensions. Active assets omit failure_reason. Unavailable assets include failure_reason and omit all these measurements. Processing assets omit both measurements and failure_reason. ### Attributes | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `asset_id` | string | Required | ID assigned to the asset by the platform. Length: 36–36. Format: ast- and 32 hex digits. | | `status` | string | Required | Current state; see the value notes for its meaning. `processing`: Processing; `active`: Asset is ready to use; `unavailable`: Unavailable; | | `object` | string | Required | Identifies the kind of object returned. Always: asset. | | `media_type` | string | Required | The kind of media in this file. `image`: Image media; `audio`: Audio media; `video`: Video media; | | `model` | string | Required | ID of the model used for this request. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `name` | string | Optional | Optional display name. Control characters, line or paragraph separators and bidirectional text controls are not accepted. Length: 1–64. | | `created_at` | integer | Required | Creation time as whole seconds since the Unix epoch. Range: ≥ 0. | | `updated_at` | integer | Required | Most recent update time as whole Unix seconds. Range: ≥ 0. | | `width` | integer | Optional | Width in pixels, required for active images and videos. Omitted for audio, processing assets and unavailable assets. Range: ≥ 1. | | `height` | integer | Optional | Height in pixels, required for active images and videos. Omitted for audio, processing assets and unavailable assets. Range: ≥ 1. | | `duration_ms` | integer | Optional | Measured duration in milliseconds, required for active audio and video. Omitted for images and for assets that are processing or unavailable. Range: ≥ 1. | | `size_bytes` | integer | Optional | Stored size in bytes, required when active and omitted while processing or unavailable. Range: ≥ 1. | | `sha256` | string | Optional | SHA-256 of the bytes actually stored, required when active and omitted while processing or unavailable. Length: 64–64. Format: 64 hex digits. | | `failure_reason` | string | Optional | Required reason when unavailable. Omitted while processing and when active. `source_url_not_allowed`: The source address does not meet the fetch policy; `source_fetch_failed`: The source file could not be fetched; `source_too_large`: The source file is too large; `media_type_mismatch`: The media type does not match; `media_constraints_violated`: The file does not meet the media requirements; `review_rejected`: The asset did not pass review; `review_unavailable`: Asset review is temporarily unavailable; `internal_error`: The service could not complete the request; | ### Example ```json { "asset_id": "ast-11111111111111111111111111111111", "object": "asset", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference", "created_at": 1700000000, "updated_at": 1700000100, "status": "processing" } ``` Read this object with [Check an asset](./api-docs-en-api-assets-retrieve.md). --- ## Register an asset `POST /v1/assets` Requires service.use. Register an image, audio clip or video at a public HTTPS address. After acceptance, the file is fetched and checked asynchronously. Idempotency belongs to your team and this operation: the same request returns the saved response; a different request with the same value is rejected. Models without asset input support return invalid_request without creating an asset, intake job or reservation. - Put your API key after `Authorization: Bearer`; the key needs `service.use` permission. - Send an `Idempotency-Key`; the same request replays the original response and a different one is refused. - After acceptance the file is fetched and checked in the background; read its status at `Location`. - Only `active` assets can be referenced with `asset://` in generation requests. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Authorization` | string | Required | Bearer, a space and your API key. A missing or invalid key returns 401 authentication_failed. | | `Content-Type` | string | Required | The body is JSON: application/json. | | `X-Request-ID` | string | Optional | Your own request identifier. An invalid or repeated value is replaced; the value used comes back in the X-Request-ID response header. | | `Idempotency-Key` | string | Required | Generate a new value for every new request and save it; it starts with a letter or digit. When the outcome is unknown, retry with the same key and the original body to get the original response; the same key with a different body returns 409 idempotency_conflict. Keys belong to your team, not to one API key. Length: 8–128. Characters: A–Z a–z 0–9 . _ : -. | ### Request body · application/json · Required The body accepts only the fields below; any other field is rejected. | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `url` | string | Required | Public HTTPS address of the source file, up to 2048 UTF-8 bytes. User information in the URL is rejected, as are destinations outside the fetch policy. Length: 9–2048. Format: https://. | | `media_type` | string | Required | The kind of media in this file. `image`: Image media; `audio`: Audio media; `video`: Video media; | | `model` | string | Required | ID of the model used for this request. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `name` | string | Optional | Optional display name. Control characters, line or paragraph separators and bidirectional text controls are not accepted. Length: 1–64. | ### Returns · HTTP 202 Asset intake was accepted. Follow Location to check its progress. | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `asset_id` | string | Required | ID assigned to the asset by the platform. Length: 36–36. Format: ast- and 32 hex digits. | | `status` | string | Required | Current state; see the value notes for its meaning. Always: processing. | #### Response headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `X-Request-ID` | string | Required | The identifier of this request: the valid value you sent, or its replacement. | | `Cache-Control` | string | Required | Instructs clients not to cache the response. | | `Location` | string | Required | Canonical relative lookup path for the accepted object. | ### Status codes Each `error.code` and what to do about it is on [Error codes](./api-docs-en-api-errors.md). | Status | Meaning | error.code | | --- | --- | --- | | 202 | Asset intake was accepted. Response headers: `Cache-Control`, `Location` | | | 400 | The request is invalid or the source address fails the HTTPS fetch policy. | invalid_request, source_url_not_allowed | | 401 | The API key is missing or invalid. Response headers: `WWW-Authenticate` | authentication_failed | | 403 | This API key does not have service.use permission. | permission_denied | | 409 | Your team already used this idempotency value for a different canonical asset registration request. | idempotency_conflict | | 410 | The saved acceptance response is too old to replay exactly. | idempotency_replay_expired | | 429 | The asset cannot be accepted within your team's stored-byte allowance or intake capacity. | asset_intake_rate_limit_exceeded, asset_storage_quota_exceeded | | 503 | The service is temporarily unavailable. | dependency_unavailable | ### Related guides - [Upload assets](./api-docs-en-assets.md) - [Idempotency and retries](./api-docs-en-idempotency.md) ### Request examples ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "Save IDEMPOTENCY_KEY for retries: ${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/assets" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://images.example.test/reference.png", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference" }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("Save IDEMPOTENCY_KEY for retries: " + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/assets" body_text = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("Save IDEMPOTENCY_KEY for retries: " + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/assets"; let bodyText = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/assets" bodyText := "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "Save IDEMPOTENCY_KEY for retries: "+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### Response examples 202 ```json { "asset_id": "ast-11111111111111111111111111111111", "status": "processing" } ``` 400 ```json { "error": { "code": "invalid_request", "message": "invalid asset request" }, "trace_id": "trace_asset_invalid" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 403 ```json { "error": { "code": "permission_denied", "message": "permission denied" }, "trace_id": "trace_asset_permission" } ``` 409 ```json { "error": { "code": "idempotency_conflict", "message": "idempotency key conflicts with the original request" }, "trace_id": "trace_asset_conflict" } ``` 410 ```json { "error": { "code": "idempotency_replay_expired", "message": "asset create replay expired" }, "trace_id": "trace_asset_expired" } ``` 429 ```json { "error": { "code": "asset_storage_quota_exceeded", "message": "asset storage quota exceeded" }, "trace_id": "trace_asset_quota" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## Check an asset `GET /v1/assets/{asset_id}` Requires service.use and an asset owned by your team. Unknown IDs, deleted assets and assets owned by another team all return asset_not_found. Active assets include their size and digest, plus dimensions or duration where applicable. These measurements are omitted during processing or when unavailable. The original source address is never returned. - Put your API key after `Authorization: Bearer`; the key needs `service.use` permission. - Only your team's assets are visible; any other ID returns `asset_not_found`. - `active` assets return dimensions, size and digest; for `unavailable`, read `failure_reason`. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Authorization` | string | Required | Bearer, a space and your API key. A missing or invalid key returns 401 authentication_failed. | | `X-Request-ID` | string | Optional | Your own request identifier. An invalid or repeated value is replaced; the value used comes back in the X-Request-ID response header. | ### Path parameters | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `asset_id` | string | Required | ID of an asset owned by the signed-in team. Length: 36–36. Format: ast- and 32 hex digits. | ### Returns · HTTP 200 The asset's current state and the metadata safe to return at that state. Returns [The asset object](./api-docs-en-api-assets-object.md); its fields are described there. #### Response headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `X-Request-ID` | string | Required | The identifier of this request: the valid value you sent, or its replacement. | | `Cache-Control` | string | Required | Instructs clients not to cache the response. | ### Status codes Each `error.code` and what to do about it is on [Error codes](./api-docs-en-api-errors.md). | Status | Meaning | error.code | | --- | --- | --- | | 200 | The asset's current state and the metadata safe to return at that state. Response headers: `Cache-Control` | | | 400 | The submitted request is not valid. | invalid_request | | 401 | The API key is missing or invalid. Response headers: `WWW-Authenticate` | authentication_failed | | 403 | This API key does not have service.use permission. | permission_denied | | 404 | No visible asset matches this ID. | asset_not_found | | 503 | The service is temporarily unavailable. | dependency_unavailable | ### Related guides - [Upload assets](./api-docs-en-assets.md) - [Generate a video](./api-docs-en-video-generate.md) ### Request examples ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/assets/${ASSET_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} ASSET_ID = os.environ["ASSET_ID"] if not re.fullmatch("^ast-[0-9a-f]{32}$", ASSET_ID): sys.exit("Set ASSET_ID to the ID from the creation response") path = "/v1/assets/{asset_id}".replace("{asset_id}", urllib.parse.quote(ASSET_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // Needs Node.js 20 or later; save it as a .mjs file and run it. const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const ASSET_ID = process.env.ASSET_ID; if (!ASSET_ID) throw new Error("Set ASSET_ID"); if (typeof ASSET_ID !== "string" || (new RegExp("^ast-[0-9a-f]{32}$")).exec(ASSET_ID)?.[0] !== ASSET_ID) throw new Error("Set ASSET_ID to the ID from the creation response"); const path = "/v1/assets/{asset_id}".replace("{asset_id}", encodeURIComponent(ASSET_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } assetID := os.Getenv("ASSET_ID") if assetID == "" { fail("Set ASSET_ID") } if !regexp.MustCompile(`\A(?:ast-[0-9a-f]{32})\z`).MatchString(assetID) { fail("Set ASSET_ID to the ID from the creation response") } path := strings.Replace("/v1/assets/{asset_id}", "{asset_id}", url.PathEscape(assetID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### Response examples 200 ```json { "asset_id": "ast-11111111111111111111111111111111", "object": "asset", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference", "created_at": 1700000000, "updated_at": 1700000100, "status": "processing" } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 403 ```json { "error": { "code": "permission_denied", "message": "permission denied" }, "trace_id": "trace_asset_permission" } ``` 404 ```json { "error": { "code": "asset_not_found", "message": "asset not found" }, "trace_id": "trace_asset_missing" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## Receive a task completion notification `POST task.completed` Chenvoya sends task.completed when a task reaches succeeded, failed or cancelled. Successful tasks also wait for result delivery processing to finish. Delivery is at least once: retries keep the same event ID and original body bytes. Notifications have no download links. Read the task for current status and results. See Callbacks for signature verification, deduplication and retries. - Any 2xx response confirms receipt; other answers and timeouts are retried on schedule. - Verify `Chenvoya-Signature` before you handle the body. - Delivery is at least once: deduplicate on `event_id` in the verified body. - The event has no download link; read the task for the result. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Chenvoya-Event-Id` | string | Required | A convenience copy of the event ID. This header is not covered by the signature. After verifying the signature, deduplicate using event_id inside the signed body. Format: uuid. | | `Chenvoya-Signature` | string | Required | The notification signature contains t and v1. Check the timestamp as described in Callbacks, calculate HMAC-SHA256 over the original body bytes and compare in constant time. | ### Request body · application/json · Required | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `event_id` | string | Required | The unique event ID inside the signed body. Deduplicate on this field after successful signature verification. Format: uuid. | | `object` | string | Required | The notification object type, always event. Always: event. | | `type` | string | Required | Whether this notification reports task completion or billing settlement. `task.completed`: The task reached a final state; `task.billing_settled`: The task billing has been settled; | | `schema_version` | integer | Required | The notification format version, currently the integer 1. Always: 1. | | `created_at` | integer | Required | Event creation time in whole Unix seconds. Check it separately from each delivery attempt's signature timestamp. Range: ≥ 0. | | `data` | object | Required | The customer task snapshot saved when this event was created. | | `data.task_id` | string | Required | The platform Task ID belonging to your team. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.request_id` | string | Required | The Request ID associated with task creation. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.billing_correlation_id` | string | Required | The billing correlation ID associated with task creation. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.model` | string | Required | The public model ID used for this task. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `data.status` | string | Required | The final task status: succeeded, failed or cancelled. `succeeded`: Succeeded; `failed`: Failed; `cancelled`: Cancelled; | | `data.delivery_status` | string | Required | Result delivery status when the event snapshot was saved. `pending`: Not yet delivered; `materializing`: Preparing the download; `available`: Ready to download; `output_unavailable`: Result unavailable; | | `data.error` | object | Required, nullable | The task error object, or null when there is no error. | | `data.error.code` | string | Required | A code your application can use to handle the error. `no_common_asset_route`: Assets temporarily unavailable; `asset_not_ready`: A referenced asset is still processing; `asset_review_timeout`: Asset review timed out; `route_misconfigured`: Service configuration error; `route_temporarily_unavailable`: Temporarily unavailable; `generation_expired`: Generation expired; `generation_failed`: Generation failed; `input_fetch_failed`: An input could not be downloaded from its URL; `input_url_not_allowed`: An input URL is not allowed; `input_too_large`: An input exceeds its size limit; `input_unsupported`: An input format, dimensions or duration is unsupported; `input_rejected`: An input did not pass content review; `input_preparation_failed`: The inputs could not be prepared in time; `input_duration_exceeded`: The combined input duration exceeds the model limit; | | `data.error.message` | string | Required | A readable explanation of the error. Length: 1–1024. | | `data.error.details` | object | Optional | Optional fields with more information about the error. | | `data.error.details.content_index` | integer | Optional | Zero-based position of the failed input in the original content array, including text items. Range: 0–50. | | `data.settlement_status` | string | Required | The billing settlement status when the event snapshot was saved. `reserved`: Reserved; `pending`: Pending; `reconciliation_required`: Under review; `settled`: Settled; `settled_with_receivable`: Settled - amount due; | | `data.final_actual_cost` | string | Required, nullable | The final charge as a USD string with six decimal places, or null until determined. Format: 6 decimal places. | ### Your response The notification was received. Any 2xx response confirms delivery; other responses and timeouts cause scheduled retries. #### Retry schedule - after 10 seconds - after 1 minute - after 5 minutes - then every hour - for up to 7 days #### Verify the signature The full signature, deduplication and age rules are on [Verify callback signatures](./api-docs-en-callbacks-verify.md). ```python import base64 import hashlib import hmac import time def verify(signing_secret, header, body): fields = {} for item in header.split(","): name, _, value = item.strip().partition("=") fields[name] = value t, v1 = fields.get("t", ""), fields.get("v1", "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False key = base64.urlsafe_b64decode(signing_secret + "=" * (-len(signing_secret) % 4)) signed = b"chenvoya.v1." + t.encode() + b"." + body expected = hmac.new(key, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ### Related guides - [Receive callbacks](./api-docs-en-callbacks.md) - [Verify callback signatures](./api-docs-en-callbacks-verify.md) ### Example body ```json { "event_id": "7f9c2ba4-e88f-4d2b-9a2f-0c3e5b8d1a64", "object": "event", "type": "task.completed", "schema_version": 1, "created_at": 1793167400, "data": { "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "delivery_status": "available", "error": null, "settlement_status": "pending", "final_actual_cost": null } } ``` --- ## Receive a billing settlement notification `POST task.billing_settled` Chenvoya sends task.billing_settled after task.completed is delivered or abandoned and settlement reaches settled or settled_with_receivable. This event has its own seven-day delivery window. No billing notification is created if settlement is still pending thirty days after completion. The final charge matches the task query; it is zero for settled failed or cancelled tasks. - Any 2xx response confirms receipt; other answers and timeouts are retried on schedule. - Signature, deduplication and retry rules are those of `task.completed`. - Sent after `task.completed` is delivered or abandoned and settlement is final. - A task still unsettled thirty days after completion gets no such event. ### Headers | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `Chenvoya-Event-Id` | string | Required | A convenience copy of the event ID. This header is not covered by the signature. After verifying the signature, deduplicate using event_id inside the signed body. Format: uuid. | | `Chenvoya-Signature` | string | Required | The notification signature contains t and v1. Check the timestamp as described in Callbacks, calculate HMAC-SHA256 over the original body bytes and compare in constant time. | ### Request body · application/json · Required | Field | Type | Requirement | Description | | --- | --- | --- | --- | | `event_id` | string | Required | The unique event ID inside the signed body. Deduplicate on this field after successful signature verification. Format: uuid. | | `object` | string | Required | The notification object type, always event. Always: event. | | `type` | string | Required | Whether this notification reports task completion or billing settlement. `task.completed`: The task reached a final state; `task.billing_settled`: The task billing has been settled; | | `schema_version` | integer | Required | The notification format version, currently the integer 1. Always: 1. | | `created_at` | integer | Required | Event creation time in whole Unix seconds. Check it separately from each delivery attempt's signature timestamp. Range: ≥ 0. | | `data` | object | Required | The customer task snapshot saved when this event was created. | | `data.task_id` | string | Required | The platform Task ID belonging to your team. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.request_id` | string | Required | The Request ID associated with task creation. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.billing_correlation_id` | string | Required | The billing correlation ID associated with task creation. Length: 4–128. Characters: A–Z a–z 0–9 _ -. | | `data.model` | string | Required | The public model ID used for this task. `dreamina-seedance-2-0-260128`: Seedance 2.0; `dreamina-seedance-2-0-fast-260128`: Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`: Seedance 2.0 Mini; | | `data.status` | string | Required | The final task status: succeeded, failed or cancelled. `succeeded`: Succeeded; `failed`: Failed; `cancelled`: Cancelled; | | `data.delivery_status` | string | Required | Result delivery status when the event snapshot was saved. `pending`: Not yet delivered; `materializing`: Preparing the download; `available`: Ready to download; `output_unavailable`: Result unavailable; | | `data.error` | object | Required, nullable | The task error object, or null when there is no error. | | `data.error.code` | string | Required | A code your application can use to handle the error. `no_common_asset_route`: Assets temporarily unavailable; `asset_not_ready`: A referenced asset is still processing; `asset_review_timeout`: Asset review timed out; `route_misconfigured`: Service configuration error; `route_temporarily_unavailable`: Temporarily unavailable; `generation_expired`: Generation expired; `generation_failed`: Generation failed; `input_fetch_failed`: An input could not be downloaded from its URL; `input_url_not_allowed`: An input URL is not allowed; `input_too_large`: An input exceeds its size limit; `input_unsupported`: An input format, dimensions or duration is unsupported; `input_rejected`: An input did not pass content review; `input_preparation_failed`: The inputs could not be prepared in time; `input_duration_exceeded`: The combined input duration exceeds the model limit; | | `data.error.message` | string | Required | A readable explanation of the error. Length: 1–1024. | | `data.error.details` | object | Optional | Optional fields with more information about the error. | | `data.error.details.content_index` | integer | Optional | Zero-based position of the failed input in the original content array, including text items. Range: 0–50. | | `data.settlement_status` | string | Required | The billing settlement status when the event snapshot was saved. `reserved`: Reserved; `pending`: Pending; `reconciliation_required`: Under review; `settled`: Settled; `settled_with_receivable`: Settled - amount due; | | `data.final_actual_cost` | string | Required, nullable | The final charge as a USD string with six decimal places, or null until determined. Format: 6 decimal places. | ### Your response The notification was received. Any 2xx response confirms delivery; other responses and timeouts cause scheduled retries. #### Retry schedule - after 10 seconds - after 1 minute - after 5 minutes - then every hour - for up to 7 days #### Verify the signature The full signature, deduplication and age rules are on [Verify callback signatures](./api-docs-en-callbacks-verify.md). ```python import base64 import hashlib import hmac import time def verify(signing_secret, header, body): fields = {} for item in header.split(","): name, _, value = item.strip().partition("=") fields[name] = value t, v1 = fields.get("t", ""), fields.get("v1", "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False key = base64.urlsafe_b64decode(signing_secret + "=" * (-len(signing_secret) % 4)) signed = b"chenvoya.v1." + t.encode() + b"." + body expected = hmac.new(key, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ### Related guides - [Receive callbacks](./api-docs-en-callbacks.md) - [How billing works](./api-docs-en-billing.md) ### Example body ```json { "event_id": "7f9c2ba4-e88f-4d2b-9a2f-0c3e5b8d1a64", "object": "event", "type": "task.billing_settled", "schema_version": 1, "created_at": 1793167400, "data": { "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "delivery_status": "available", "error": null, "settlement_status": "settled", "final_actual_cost": "0.361564" } } ``` --- ## Error codes Every error.code with its meaning, status and endpoints. | Status | error.code | Meaning | What to do | Retry as is | Charged | | --- | --- | --- | --- | --- | --- | | 400, 404 | `asset_not_found` | Asset is unknown, deleted or outside your team (from Check an asset, Create a video task) | Check that the asset ID belongs to your team and still exists. | No | No | | 400 | `asset_unusable` | Asset failed, has an unsuitable media type or lacks review approval for this request (from Create a video task) | Use another asset, or one whose media type fits its role. | No | No | | 400 | `callback_target_not_verified` | The address does not exactly match a verified, unrevoked and unexpired callback target owned by your team (from Create a video task) | Register and verify the callback address in the console, or drop `callback_url`. | No | No | | 400 | `input_video_duration_exceeded` | Combined video input exceeds the model's duration limit (from Create a video task) | Shorten the reference videos to the model's total limit. | No | No | | 400 | `invalid_request` | Invalid request, including too many references for the model (from Register an asset, Check an asset, Create a video task, List team tasks, Read a video task, Cancel a video task, Read task billing) | Fix the request as `error.message` describes, then send it again. | No | No | | 400 | `source_url_not_allowed` | The source address does not meet the fetch policy (from Register an asset) | Use a public HTTPS address that the fetch policy accepts. | No | No | | 400 | `unsupported_capability` | The available choices cannot satisfy the required capabilities together (from Create a video task) | Adjust the parameters named in `error.details`. | No | No | | 401 | `authentication_failed` | The request could not be authenticated (from Register an asset, Check an asset, Create a video task, List team tasks, Read a video task, Cancel a video task, Read task billing) | Check the API key after `Authorization: Bearer`. | No | No | | 403 | `permission_denied` | Required permission is missing (from Register an asset, Check an asset) | Use an API key that has `service.use` permission. | No | No | | 404 | `task_not_found` | The task is unknown or not visible to your team (from Read a video task, Cancel a video task, Read task billing) | Check that the task ID belongs to your team. | No | No | | 409 | `asset_not_ready` | A referenced asset is still processing (from Create a video task) | Wait until the asset is `active`, then retry with the same key and body. | After the cause is fixed | No | | 409 | `idempotency_conflict` | The same idempotency value was used for a different request (from Register an asset, Create a video task, Cancel a video task) | Retry the original body with the same key; send a new request with a new key. | No | No | | 409, 410 | `idempotency_replay_expired` | The saved response can no longer be replayed exactly (from Register an asset, Create a video task, Cancel a video task) | The original request was accepted: read it by ID instead of sending it again with a new key. | No | No | | 409 | `key_provisioning_incomplete` | API key setup is incomplete; delete the key and create another (from Create a video task) | Delete this API key and create a new one. | No | No | | 409 | `task_not_cancellable` | This finished task has no cancellation operation to refer to (from Cancel a video task) | The task has already finished; there is nothing to cancel. | No | No | | 429 | `asset_intake_rate_limit_exceeded` | Asset intake capacity is limited (from Register an asset) | Retry later with the same key. | After the cause is fixed | No | | 429 | `asset_storage_quota_exceeded` | Asset storage allowance is insufficient (from Register an asset, Create a video task) | The storage allowance is used up; free space or contact us before registering more. | No | No | | 429 | `insufficient_balance` | Balance is insufficient to accept the request (from Create a video task) | Top up, then retry with the same key; `error.details` gives the reservation needed. | After the cause is fixed | No | | 429 | `rate_limited` | The request rate is limited (from Create a video task, List team tasks, Read a video task, Cancel a video task, Read task billing) | Wait `Retry-After` seconds, then retry with the same key. | After the cause is fixed | No | | 500 | `internal_error` | The service could not complete the request (from Create a video task, List team tasks, Read a video task, Cancel a video task, Read task billing) | Retry with the same `Idempotency-Key` and body. | Yes, same key | No; a retry is never charged twice | | 503 | `dependency_unavailable` | The service is temporarily unavailable and the outcome is unknown (from Register an asset, Check an asset, Create a video task, List team tasks, Read a video task, Cancel a video task, Read task billing) | The outcome is unknown: retry with the same key and body. | Yes, same key | No; a retry is never charged twice | | 503 | `funding_intake_closed` | New intake is closed (from Create a video task) | New intake is closed for now; try later. Exact replays of accepted requests still return their response. | After the cause is fixed | No | --- ## Statuses and reasons Every value of the task, delivery, settlement, asset and billing fields. ### Task status | Value | Meaning | Final | | --- | --- | --- | | `queued` | Queued | No | | `processing` | Processing | No | | `succeeded` | Succeeded | Yes | | `failed` | Failed | Yes | | `cancelled` | Cancelled | Yes | ### Execution phase | Value | Meaning | | --- | --- | | `preparing_assets` | Preparing inputs | | `waiting_route` | Waiting | | `submitting` | Starting | | `provider_processing` | Generating | | `reconciling` | Confirming the result | | `finalizing` | Finishing | | `settlement_pending` | Awaiting settlement | ### Delivery status | Value | Meaning | | --- | --- | | `pending` | Not yet delivered | | `materializing` | Preparing the download | | `available` | Ready to download | | `output_unavailable` | Result unavailable | ### Settlement status | Value | Meaning | | --- | --- | | `reserved` | Reserved | | `pending` | Pending | | `reconciliation_required` | Under review | | `settled` | Settled | | `settled_with_receivable` | Settled - amount due | ### Task error codes | Value | Meaning | | --- | --- | | `no_common_asset_route` | Assets temporarily unavailable | | `asset_not_ready` | A referenced asset is still processing | | `asset_review_timeout` | Asset review timed out | | `route_misconfigured` | Service configuration error | | `route_temporarily_unavailable` | Temporarily unavailable | | `generation_expired` | Generation expired | | `generation_failed` | Generation failed | | `input_fetch_failed` | An input could not be downloaded from its URL | | `input_url_not_allowed` | An input URL is not allowed | | `input_too_large` | An input exceeds its size limit | | `input_unsupported` | An input format, dimensions or duration is unsupported | | `input_rejected` | An input did not pass content review | | `input_preparation_failed` | The inputs could not be prepared in time | | `input_duration_exceeded` | The combined input duration exceeds the model limit | ### Asset status | Value | Meaning | | --- | --- | | `processing` | Processing | | `active` | Asset is ready to use | | `unavailable` | Unavailable | ### Asset failure reasons | Value | Meaning | | --- | --- | | `source_url_not_allowed` | The source address does not meet the fetch policy | | `source_fetch_failed` | The source file could not be fetched | | `source_too_large` | The source file is too large | | `media_type_mismatch` | The media type does not match | | `media_constraints_violated` | The file does not meet the media requirements | | `review_rejected` | The asset did not pass review | | `review_unavailable` | Asset review is temporarily unavailable | | `internal_error` | The service could not complete the request | ### Cancellation results | Value | Meaning | | --- | --- | | `requested` | Requested | | `effective` | Cancelled | | `not_effective` | Not cancelled | | `failed` | Failed | ### Billing event types | Value | Meaning | | --- | --- | | `hold_created` | Reserved | | `hold_increased` | Reservation increased | | `hold_reduced` | Reservation reduced | | `charge_captured` | Charged | | `supplemental_charge` | Additional charge | | `receivable_created` | Amount due recorded | | `receivable_payment` | Amount due paid | | `receivable_reduced` | Amount due reduced | | `hold_released` | Reservation released | | `refund_posted` | Refunded | | `charge_waived` | Charge waived | | `manual_adjustment` | Manual adjustment |