Quickstart
Generate your first video in three steps: create an API key, submit a task, get the result.
On this page
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
Reading the API address…Every request path follows this address, for example POST /v1/video/generations. The examples read it from the CHENVOYA_BASE_URL environment variable, without a trailing slash.
Create an API key
On the API keys 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.
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.
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.
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."
}
]
}'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)// 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;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.
{
"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.
Get the result
A task moves through the states below; succeeded, failed and cancelled are final.
queuedprocessingsucceededThe other final statesFailedfailedCancelledcancelledare free.
Poll the task
Read GET /v1/video/tasks/{task_id} every few seconds until status is final.
curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}" \
--header "Authorization: Bearer ${CHENVOYA_API_KEY}"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)// 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;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)
}{
"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:
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 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 before you handle an event.
"callback_url": "https://example.com/webhooks/chenvoya"