Image / video / music APIs
/v1/images/generations billed per image · /v1/videos/generations async, billed per second · /v1/music/generations async, billed per generation · /v1/transcripts/extract async video-to-script, billed per call
Alongside the four chat protocol endpoints, the platform offers three families of media endpoints. Authentication is exactly as it is for chat — the same sk-gpushare-* key, in whichever of the four forms you like (x-api-key or x-goog-api-key header, ?key= query, Authorization: Bearer); see Authentication. Everything bills against your account balance (shared by all keys), and an insufficient balance returns 402 quota_exceeded.
| Endpoint | Purpose | Billing |
|---|---|---|
POST /v1/images/generations | Text-to-image and image-to-image (synchronous) | per image |
POST /v1/videos/generations | Text-to-video and image-to-video (async task) | per second |
GET /v1/videos/generations/{id} | Poll a video task | free |
GET /v1/videos/generations | List this account's video tasks (all statuses by default; ?limit= default 30, max 100; ?status= to filter) | free |
POST /v1/music/generations | AI music generation (Suno, async task) | per generation (2 songs each) |
GET /v1/music/generations/{id} | Poll a music task | free |
GET /v1/music/generations | List this account's music tasks (same shape) | free |
POST /v1/audio/speech | Speech synthesis (synchronous, or "async": true for a task) | per character |
GET /v1/audio/speech/{id} | Poll a speech task | free |
GET /v1/audio/speech | List this account's speech tasks (same shape) | free |
POST /v1/transcripts/extract | Short-video link → spoken script (synchronous) | per call |
Every response from these endpoints carries an
x-gateway-traceheader — include it with the timestamp, the model and the full error body when reporting a problem.
Retries don't double-charge: Idempotency-Key#
Every billed POST endpoint (image, video, music, speech, voice clone, digital-human avatar, transcript) accepts an Idempotency-Key request header. With it, the same request can be sent any number of times and is executed exactly once:
IDEM=$(uuidgen) # one key per submit intent, reused by every retry of it
curl https://jiuye.zsopc.com/v1/videos/generations \
-H "Authorization: Bearer $PLATFORM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM" \
-d '{"model":"doubao-seedance-2-0-260128","prompt":"sunset over the sea","duration":5}'
- Same key + same request body → the original response is returned verbatim (same task
id), with an extraIdempotency-Replayed: trueheader. No second task, no second charge. - This is also the easiest way to recover a task id: forgot to save the
id? Re-run the exact same curl (same key, same body) and you get the original task back. - Keys are retained for 7 days; after that the same key counts as a new request.
- Key format: 1–200 printable ASCII characters (a UUID is the obvious choice). One key per submit intent — do not reuse it across different requests.
- Scoped to the account, not to a single API key: replaying the same
Idempotency-Keyfrom a differentsk-gpushare-*key on the same account still hits the replay. (The wallet is shared account-wide, so anything narrower would leak duplicate charges.) - Concurrency: when two requests carrying the same key arrive at once, exactly one executes and the other immediately gets 409
idempotency_in_flight— they never both run.
import uuid, requests
idem = str(uuid.uuid4()) # one key per submit intent
body = {
"model": "doubao-seedance-2-0-260128",
"content": [{"type": "text", "text": "sunset over the sea, drone shot"}],
"duration": 5,
}
def submit():
r = requests.post(
"https://jiuye.zsopc.com/v1/videos/generations",
headers={
"Authorization": f"Bearer {PLATFORM_API_KEY}",
"Idempotency-Key": idem, # same key on every retry
},
json=body,
timeout=60,
)
r.raise_for_status()
# "true" on a replay: you got the first call's result, with no second charge
replayed = r.headers.get("Idempotency-Replayed") == "true"
return r.json()["id"], replayed
task_id, _ = submit()
task_id_again, replayed = submit() # lost the id? just call it again
assert task_id == task_id_again and replayed
const idem = crypto.randomUUID(); // one key per submit intent
async function submit() {
const res = await fetch("https://jiuye.zsopc.com/v1/videos/generations", {
method: "POST",
headers: {
Authorization: `Bearer ${PLATFORM_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idem, // same key on every retry
},
body: JSON.stringify({
model: "doubao-seedance-2-0-260128",
content: [{ type: "text", text: "sunset over the sea, drone shot" }],
duration: 5,
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return {
id: (await res.json()).id,
replayed: res.headers.get("Idempotency-Replayed") === "true",
};
}
| Case | Response |
|---|---|
| Header absent | Behaves exactly as before (no deduplication at all) |
| Malformed key | 400 invalid_idempotency_key |
| Same key, different body | 409 idempotency_key_reuse — use a fresh key |
| Same key, previous call still running | 409 idempotency_in_flight — wait and retry with the same key (a new key really would submit again). Usually a few seconds; if the previous call was cut off client-side, the key is held for up to 10 minutes before it frees itself |
| Same key, original response too large to retain | 409 idempotency_response_not_cached — only reachable for image calls that explicitly ask for response_format:"b64_json" (the bytes are too large to retain). Use the default response_format:"url" and replay works normally; the call that already happened can only be re-sent under a fresh key (and is billed again) |
Only 2xx responses are remembered. Upstream errors and validation failures do not consume the key — retry with the same one.
Recovering a task id#
Async tasks (video, music, speech) return a task id on submit. If you didn't save it, there are three ways back, easiest first:
- Re-send the exact same curl (same
Idempotency-Key) → you get the original task id straight back. See the section above. - List your tasks:
GET /v1/videos/generations,GET /v1/music/generations,GET /v1/audio/speech. All statuses are returned by default (including queued, running and failed), newest first;?limit=defaults to 30, max 100.curl "https://jiuye.zsopc.com/v1/videos/generations?limit=10" \ -H "Authorization: Bearer $PLATFORM_API_KEY"?status=filters, comma-separated for multiple: video acceptsqueued,running,succeeded,failed,expired,cancelled,all; musicprocessing,succeeded,failed,expired,cancelled,all; speechpending,succeeded,failed,all. Pass?status=succeededfor the pre-2026-07-31 default. - The call log site logs.dflop.top: the search box matches both task ID and request ID (paste it in — you don't have to know which kind of id you're holding). Async-task records (video, music, speech, avatar, voice) carry a task ID; calls with no task row behind them (chat, image, transcript) carry only a request ID.
- The request ID is the value of the
x-gateway-traceresponse header. - ⚠️ An async task is only recorded here once it reaches a terminal state and settles; for a task still generating, use the list endpoints in point 2. The log defaults to the last 30 days.
- The request ID is the value of the
POST /v1/images/generations#
OpenAI Images API-compatible shape, returned synchronously.
Available models#
| Model ID | Display name | Price (per image) | Notes |
|---|---|---|---|
doubao-seedream-4-0-250828 | Seedream 4.0 | 11.73 | size ≥ 960×960 |
doubao-seedream-4-5-251128 | Seedream 4.5 | 14.96 | size must be ≥ 1920×1920, or upstream returns 400 |
doubao-seedream-5-0-260128 | Seedream 5.0 | 12.94 | size must be ≥ 1920×1920, or upstream returns 400 |
doubao-seedream-5-0-pro-260628 | Seedream 5.0 Pro | 17.79 for output ≤ 2.36 MP, 35.59 above that | size ≥ 960×960. Omitting size means upstream defaults to 2048×2048 and you pay the 35.59 tier — to stay on the lower tier, pass a size ≤ 2.36 MP explicitly (e.g. 1536x1536). Each image[] reference adds 1.21 of input (on the same billing line) |
grok-imagine-image | Grok Imagine (Image) | 28.31 | standard tier |
grok-imagine-image-quality | Grok Imagine (Quality) | 28.31 | high-quality tier |
sizeis passed straight through and never rewritten by the gateway — asking Seedream 4.5/5.0 for less than 1920×1920 gets you the upstream's 400 directly. Seedream 5.0 Pro is tiered on the requested output pixel area (the threshold is 2.36 MP ≈ 1536×1536).
The request#
{
"model": "doubao-seedream-4-5-251128",
"prompt": "A panda drinking tea in a bamboo grove, watercolour style",
"size": "2048x2048",
"n": 1
}
| Field | Required | Notes |
|---|---|---|
model | ✓ | a Model ID from the table above |
prompt | ✓ | the description |
size | "WxH", passed through upstream (mind each SKU's minimum) | |
n | how many images, default 1, max 10 (more returns 400 invalid_request). unit price × n is held on submit and settled against the number actually returned | |
image | array of reference-image URLs (image-to-image; Seedream takes 1–10) |
The response#
{
"model": "doubao-seedream-4-5-251128",
"created": 1765432100,
"data": [{ "url": "https://...", "size": "2048x2048" }],
"usage": { "generated_images": 1, "output_tokens": 4096, "total_tokens": 4096 }
}
The response is the upstream's own (OpenAI Images shape); the usage fields are whatever upstream actually returns.
curl#
curl https://jiuye.zsopc.com/v1/images/generations \
-H "Authorization: Bearer $PLATFORM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "doubao-seedream-4-5-251128",
"prompt": "A panda drinking tea in a bamboo grove, watercolour style",
"size": "2048x2048"
}'
Limitations#
- The returned image URLs are upstream pre-signed links that expire in about 24 hours — download and store them promptly
- This is a synchronous endpoint. The gateway's per-attempt upstream timeout is 240 seconds (
IMAGES_UPSTREAM_TIMEOUT_SECS) and the whole channel ladder is capped at 280 seconds (IMAGES_LADDER_DEADLINE_SECS). Most SKUs finish in 5–20 seconds, butgpt-image-2measurably takes 60–215 seconds — set your client timeout to ≥ 300 seconds, or your own timeout will cut off a request the gateway is still legitimately waiting on (you are still billed, you just never receive the result) - Errors use the OpenAI shape
{"error": {"code", "message", "param", "type"}}; upstream 4xx/5xx pass through with their original status and body (and aren't billed)
The Nano Banana family (
nano-banana15.77,nano-banana-pro54.19,nano-banana-216.18 per image) also runs through this endpoint and bills per image.nano-banana-2returnsb64_json(its first hop is our own pool); the others usually return URLs. The older ids (gemini-2.5-flash-image,gemini-3-pro-image-preview,gemini-3.1-flash-image(-preview),tvod-nano-*) remain supported as aliases.
POST /v1/videos/generations#
An async task: submitting returns a task id immediately, and you poll until succeeded.
Available models#
| Model ID | Display name | Price (per second) | Notes |
|---|---|---|---|
doubao-seedance-1-0-pro-fast-251015 | Seedance 1.0 Pro Fast | 32.35 | |
doubao-seedance-1-0-pro-250528 | Seedance 1.0 Pro | 60.66 | |
doubao-seedance-1-5-pro-251215 | Seedance 1.5 Pro | 72.79 | |
doubao-seedance-2-0-fast-260128 | Seedance 2.0 Fast | 48.53 | |
doubao-seedance-2-0-260128 | Seedance 2.0 | 88.97 | |
doubao-seedance-2.0 | Seedance 2.0 | by resolution: 480p 33.16 / 720p 59.45 / 1080p 147.61 / 2k 291.17 / 4k 355.87 | supports real-person photos on camera; reference images are moderated and registered automatically |
doubao-seedance-2.0-fast | Seedance 2.0 Fast | by resolution: 480p 23.86 / 720p 47.72 / 1080p 117.28 / 2k 141.54 / 4k 169.85 | |
doubao-seedance-2.0-mini | Seedance 2.0 Mini | by resolution: 480p 14.96 / 720p 29.93 | lightweight tier; 480p/720p only, 4–15 seconds |
grok-imagine-video | Grok Imagine Video | 283.08 | text-to-video and image-to-video |
grok-imagine-video-1.5-preview | Grok Imagine Video 1.5 | 586.38 | image-to-video only (upstream 400s without a reference image) |
dh-avatar | Digital human video | flat per video second (see the in-app catalogue for pricing) | requires a reusable avatar first (created from a photo or video); avatar + driving audio (or text + voice) → a talking video |
clip-realman | Smart edit · talking head | 4.04/second (of finished video) | a talking-head source video + a template → a finished cut with title, subtitles, name bar and music |
clip-mixcut | Smart edit · asset mixcut | 4.04/second (of finished video) | narration audio + image/video assets + a template → a subtitled, packaged cut |
clip-news | Smart edit · news brief | 2.43/second (of finished video) | a headline + image/video assets + a template → a news-style short, 5–300 seconds |
Submitting#
{
"model": "doubao-seedance-1-0-pro-fast-251015",
"content": [
{ "type": "text", "text": "Sunset over the sea, aerial drone view --ratio 16:9" }
],
"duration": 5
}
| Field | Required | Notes |
|---|---|---|
model | ✓ | a Model ID from the table above (grok SKUs use the same shape; the gateway translates) |
content | ✓ | an array: {type:"text", text} is mandatory; for image-to-video append {type:"image_url", image_url:{url:"https://..."}} |
duration | seconds. Omitted, we hold 12 seconds' worth (Seedance's ceiling) and settle on the real length — pass it explicitly | |
ratio / resolution / watermark | passed through upstream (Seedance's own semantics). ⚠️ resolution is required on the Lite (-lite) SKUs: the whole card prices on delivered resolution, so omitting it returns 400; the vocabulary is 720p / 1080p only, and anything else 400s too | |
video_mode | only meaningful on grok SKUs with a reference video: "extend" continues it, anything else (or omitted) rewrites it (see below) |
How grok video is translated: grok SKUs run on an xAI upstream, and the gateway converts the Seedance shape above into xAI's. Appending
{type:"video_url", video_url:{url:"https://..."}}routes the task to the video-to-video endpoint, which rewrites by default (redrawing the whole clip from your prompt, keeping the source aspect ratio and resolution, ignoring a customduration);video_mode: "extend"switches to continuation (carrying on from the last frame fordurationseconds, 2–10). For image-to-video the gateway deliberately withholdsratioso the source image's native aspect ratio is preserved and nothing gets stretched.
Real people on camera#
doubao-seedance-2.0, doubao-seedance-2.0-fast and doubao-seedance-2.0-mini support uploading a photo of a real person and generating video of them on camera. The compliance path runs entirely inside the gateway and needs nothing special from you — submit the ordinary image-to-video shape and the gateway moderates and registers the reference image (swapping in a compliant asset handle) before generating. If the preferred channel refuses a real-person image, the gateway transparently switches to one that supports registering real-person assets.
{
"model": "doubao-seedance-2.0",
"resolution": "720p",
"duration": 5,
"content": [
{ "type": "text", "text": "The person in the photo smiles and waves at the camera, background unchanged" },
{ "type": "image_url", "image_url": { "url": "https://your-cdn.com/face.jpg" } }
],
"portrait_auth": true
}
| Field | Notes |
|---|---|
image_url.url | must be a publicly fetchable http(s) URL (the upstream pulls it from the open internet). base64 / data: inline images are not supported and are rejected with 400. The image must be reachable from mainland China (our own r2.dflop.top links work; some overseas hosts can't be fetched). |
portrait_auth | Optional boolean asserting "I have the portrait rights for the real person shown", recorded for audit. It does not affect whether video is produced (real-person routing follows the SKU), but passing true explicitly is recommended whenever real people are involved, as a statement of responsibility. |
resolution | The real-person vocabulary: doubao-seedance-2.0 takes 480p/720p/1080p/2k/4k, -fast takes 480p/720p/1080p, -mini only 480p/720p. Omitted, upstream picks its default and the flat rate applies. |
Multimodal references (Seedance 2.0 only): beyond a single first-frame image,
content[]can carry reference media items tagged with arole—{type:"image_url", role:"reference_image", image_url:{url}},{type:"video_url", role:"reference_video", video_url:{url}}and{type:"audio_url", role:"reference_audio", audio_url:{url}}(up to 10 reference images). Every external image follows the same public-URL and automatic-moderation rules above.
Response:
{ "id": "9f2c...", "status": "queued", "model": "doubao-seedance-1-0-pro-fast-251015", "created_at": 1765432100 }
Submission itself is a synchronous HTTP call (with a 60-second gateway-to-upstream timeout); generation then proceeds in the background and doesn't hold the request open.
Digital-human fields (dh-avatar)#
dh-avatar reuses the same video submission endpoint, adding the fields below at the top level of the body (duration is required — the estimated seconds of the driving audio or script; omitting it returns 400). ⚠️ Digital human is a two-stage process: you need a reusable avatar first, created in the app under "Clone avatar" from a photo or video (the platform's shared avatars have been withdrawn from the UI because upstream stopped returning previews). Calling with an sk-key, just pass an existing avatar id.
The video is as long as its driving audio or script (upstream measures the real length; there's no fixed cap).
durationonly sizes the billing hold, and settlement uses upstream's actual seconds.
| Field | Required | Notes |
|---|---|---|
avatar | ✓ | the avatar id (created once under "Clone avatar" in the app, then reusable) |
audio_url | one of the two | driving audio (public URL, mp3/wav) — makes the avatar speak it |
voice + text | one of the two | text-driven: voice is a voice id (shared or cloned) and text the script (≤10,000 characters); upstream synthesises it and drives the avatar in one step |
title | a name for the piece (≤20 characters) |
Things to note:
- input media URLs must be publicly reachable (anything you've uploaded to our
r2.dflop.topworks directly); - finished videos carry an automatic "AI generated" mark, as China's AIGC labelling rules require.
Smart-edit fields (clip-realman / clip-mixcut / clip-news)#
The three smart-edit SKUs reuse the same video submission endpoint, adding the fields below at the top level. All three share style_id (the template id), title, language, materials[], bgm and cover_url, each with its own extra requirements. The length of the finished video comes from the source media (realman from the source video, mixcut from the narration audio, news from duration); for realman and mixcut, duration is only a billing hint and is never sent upstream.
{
"model": "clip-realman",
"style_id": "tpl_xxx",
"title": "Today's headlines",
"source_video_url": "https://your-cdn.com/talk.mp4",
"materials": [
{ "type": "image", "file_url": "https://your-cdn.com/a.jpg" },
{ "type": "video", "file_url": "https://your-cdn.com/b.mp4", "sound_switch": false }
],
"bgm": { "mode": "auto" }
}
| Field | Applies to | Notes |
|---|---|---|
style_id | all ✓ | template id (from the platform's smart-edit template library) |
source_video_url | realman ✓ | the talking-head source video (public URL) |
audio_url | mixcut ✓ | the narration audio (public URL) |
materials | mixcut/news ✓, optional for realman | an array of {type:"image"|"video", file_url, sound_switch?}, at most 10 |
title | news ✓, optional elsewhere | the piece's or story's headline |
duration | news | target length in seconds, 5–300 (clamped if out of range); a billing hint only for realman/mixcut |
material_composition | news | random or order; random by default |
preprocess | realman | asset pre-processing: roughCut or sliceMerge |
bgm | all | {mode:"auto"|"none"|"custom", url?, volume?}; follows the template by default |
cover_url | all | a custom first-frame cover (public image URL) |
introduce_card | all | the name bar, {name, description} |
language | all | subtitle language |
Billing: charged on the actual length of the finished video (the real seconds reported by polling). Submissions with an external sk-key hold the clip ceiling (300 seconds) and settle down to the real length on success; passing a longer
durationexplicitly (for a long source video, say) holds that instead. Insufficient balance returns 402, and failures or expiry are refunded in full.Template ids:
style_idcomes from the platform's smart-edit template library. Template discovery currently exists only inside the Digital Human studio in the app, so calling directly with an sk-key means using a template id you already know.
Asset and media requirements (upstream hard limits)#
Every URL must be publicly fetchable. The limits below are the upstream's own, and anything outside them is rejected there (the in-app Digital Human studio validates format, resolution, length and size at upload time).
| Medium | Format | Size | Resolution | Length |
|---|---|---|---|---|
Talking-head source video source_video_url | mp4 / mov (h264 or HEVC, 10–60fps, 25 recommended) | < 500MB | < 2000px per side | < 5 minutes |
Asset image materials[].file_url (image) | static jpg / png / webp | — | < 2000px per side | counts as 2s each |
Asset video materials[].file_url (video) | mp4 / mov | < 500MB | < 2000px per side | ≤ 60s each |
Narration audio audio_url (mixcut) | mp3 / wav / m4a | ≤ 120MB | — | ≤ 5 minutes, must be transcribable |
Background music bgm.url | mp3 / wav / m4a | ≤ 120MB | — | ≤ 5 minutes |
First-frame cover cover_url | jpg / jpeg / png | ≤ 10MB | < 2000px per side | — |
- Total asset length ≤ 5 minutes: images count as 2s each and videos at their real length; upstream rejects anything longer.
- The talking-head source video's audio must be transcribable (it drives the automatic subtitles); without clear speech the task fails.
clip-newsoutput length is set byduration(5–300s);clip-realmanandclip-mixcutfollow the source video and the narration audio respectively.
Polling#
curl https://jiuye.zsopc.com/v1/videos/generations/$TASK_ID \
-H "Authorization: Bearer $PLATFORM_API_KEY"
status runs queued → running → succeeded / failed / expired / cancelled.
In-flight tasks (queued / running) may carry progress (an integer 0–100 from upstream). It appears only when upstream reports progress — today just the Seedance 2.0 real-person tiers — and its absence means that model has no progress data, not 0%.
On success:
{
"id": "9f2c...",
"status": "succeeded",
"model": "doubao-seedance-1-0-pro-fast-251015",
"created_at": 1765432100,
"video_url": "https://...",
"expires_at": 1765435700
}
Failures carry error: {code, message}. Generation usually takes 1–5 minutes; poll every 5–10 seconds.
Task ids are visible only to their own account — polling a nonexistent task or someone else's returns the same 404 (code: "not_found"), with no distinction drawn.
List tasks#
GET /v1/videos/generations (no id) — this account's video tasks, newest first. Free.
Use it to recover a task id you didn't save, or just as a generation history.
| Query param | Default | Notes |
|---|---|---|
limit | 30 | 1–100; anything larger is clamped to 100 |
status | (all) | queued / running / succeeded / failed / expired / cancelled / all, comma-separated for multiple (e.g. ?status=queued,running). An unrecognised value returns 400 invalid_request |
All statuses are returned by default as of 2026-07-31. The previous default returned only
succeeded, which made the list look empty while a task was still running. Pass?status=succeededfor the old behaviour.
{
"data": [
{
"id": "9a31d5c2-5c13-4caf-ad8b-1ee70ff5887f",
"status": "running",
"model": "doubao-seedance-2.0-fast-lite",
"created_at": 1785495460,
"progress": 42
},
{
"id": "ed2ab10b-ceb1-4d0c-8c25-84ade94c95ac",
"status": "succeeded",
"model": "doubao-seedance-1-0-pro-fast-251015",
"created_at": 1785490000,
"video_url": "https://...",
"expires_at": 1786094800
}
]
}
| Field | Present when | Notes |
|---|---|---|
id | always | The task id — the same value GET /v1/videos/generations/{id} takes |
status | always | Same vocabulary as the polling endpoint |
model | always | The submitted Model ID (canonicalised) |
created_at | always | Submit time, Unix seconds |
progress | queued/running, and only when upstream reports it | Integer 0–100. An absent field means the model has no progress data — not 0% |
video_url | succeeded only | A presigned link valid for 7 days; calling this endpoint again re-signs an expired one |
expires_at | succeeded only | When video_url expires, Unix seconds |
output_files | succeeded, subtitle SKUs only | One download link per language |
error | failed/expired/cancelled only | {code, message} |
import requests
r = requests.get(
"https://jiuye.zsopc.com/v1/videos/generations",
headers={"Authorization": f"Bearer {PLATFORM_API_KEY}"},
params={"limit": 20}, # in-flight only: {"status": "queued,running"}
timeout=30,
)
for t in r.json()["data"]:
print(t["id"], t["status"], t.get("video_url", ""))
How billing works#
- On submit,
unit price × durationis held against the balance (12 seconds ifdurationis missing); an insufficient balance returns 402 - Settlement on a final state: success charges the actual length (falling back to the requested seconds when upstream doesn't report one), and failure or expiry is refunded in full; any failure during submission (an upstream error, a failed insert) releases the hold immediately
GET /v1/videos/generations(with no id) lists this account's tasks, all statuses by default (?limit=default 30, max 100;?status=succeededfor successes only) — useful both as a "generation history" and to recover a lost task id (see Recovering a task id)
Limitations#
- Successful videos are copied into our own object storage, and
video_urlis a pre-signed link valid for 7 days (expires_atis when it dies); calling the list endpoint again after expiry re-signs a fresh link - Upstream moderation can intercept after generation finishes (error codes like
OutputVideoSensitiveContentDetected); that counts as a failure and isn't billed
POST /v1/music/generations#
Suno AI music generation, with the same async task shape as video: submit for a task id, then poll to a final state. One generation yields 2 complete songs (with lyrics and cover art).
Available models#
| Model ID | Display name | Price (per generation) |
|---|---|---|
suno-v3.5 | Suno V3.5 | 19.41 |
suno-v4 | Suno V4 | 19.41 |
suno-v4.5 | Suno V4.5 | 19.41 |
suno-v5 | Suno V5 | 19.41 |
suno-v5.5 | Suno V5.5 (latest) | 19.41 |
The request#
curl https://jiuye.zsopc.com/v1/music/generations \
-H "Authorization: Bearer $GPUSHARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "suno-v5.5",
"prompt": "An upbeat Mandarin pop song about a summer walk by the sea"
}'
| Field | Type | Notes |
|---|---|---|
model | string | required; any id from the table above |
prompt | string | inspiration mode (≤200 characters): the AI writes the lyrics, picks a title and sings |
lyrics | string | your own lyrics (≤3000 characters); passing them switches to custom mode and prompt is ignored |
title | string | song title (custom mode) |
tags | string | style, e.g. "synthwave, female vocal" |
negative_tags | string | styles to avoid |
instrumental | bool | instrumental only (lyrics ignored) |
Pass at least one of prompt and lyrics (both may be omitted when instrumental: true). Response: {"id": "<task_id>", "status": "queued", "model": "...", "created_at": ...}.
Polling the task#
curl https://jiuye.zsopc.com/v1/music/generations/$TASK_ID \
-H "Authorization: Bearer $GPUSHARE_API_KEY"
status is one of processing | succeeded | failed | expired. On success the tracks array gives each song:
{
"id": "…",
"status": "succeeded",
"tracks": [
{
"clip_id": "…",
"title": "Slow Sea Breeze",
"duration_sec": 192.0,
"audio_url": "https://…mp3",
"image_url": "https://…jpeg",
"lyrics": "[Verse]…"
}
]
}
Generation usually takes 2–4 minutes; poll every 10–20 seconds. Task ids are private to their account, and someone else's or a nonexistent one always returns 404.
List tasks#
GET /v1/music/generations (no id) — this account's music tasks, newest first. Free.
Use it to recover a task id you didn't save.
| Query param | Default | Notes |
|---|---|---|
limit | 30 | 1–100 |
status | (all) | processing / succeeded / failed / expired / cancelled / all, comma-separated for multiple. ⚠️ The music family has no queued/running — everything between submit and terminal is processing (same vocabulary as the polling endpoint). An unrecognised value returns 400 |
The response is {"data": [ … ]}, and each item is field-for-field identical to the single-task polling response above (id / model / status / upstream_status / tracks[] / error_code / error_message / created_at / completed_at), so a list item can be consumed exactly like a poll result — no second parser needed.
curl "https://jiuye.zsopc.com/v1/music/generations?limit=10&status=processing" \
-H "Authorization: Bearer $PLATFORM_API_KEY"
How billing works#
- The flat unit price is held on submit (one generation = 2 songs, already included); an insufficient balance returns 402
- Settlement: one successful song is enough to charge full price; both failing, or 30 minutes without completion (expiry), is refunded in full, and any failure during submission releases the hold immediately
- Audio and cover art are copied into our own object storage, with
audio_url/image_urlas 7-day pre-signed links; if the copy fails we fall back to the upstream's original links
POST /v1/audio/speech#
Speech synthesis: text (≤5000 characters) → MP3, with cloned voices and speed adjustment (speed applies to cloned voices only). Billed per input character (voice-tts-pro, 125.36 per 1,000 characters).
⚠️ Use async mode for long scripts. Synthesis runs on an upstream async queue (seconds for short text, minutes for long), while a synchronous call is bound by the CDN's roughly 100-second ceiling on non-streaming responses — and when that cuts you off, the audio still renders and you're still charged, but you get nothing back. Add
"async": trueto the body to switch to submit-and-poll.
The request#
{
"model": "voice-tts-pro",
"input": "Hello, and welcome to speech synthesis.",
"voice": "<optional: a platform preset voice id, or a cloned voice id from /v1/audio/voices; omitted means the default voice>",
"speed": 1.0,
"async": false
}
The response (synchronous, async omitted or false)#
{
"model": "voice-tts-pro",
"audio_url": "https://r2.dflop.top/audio-speech/…/xxx.mp3",
"characters": 12,
"cost_usd": "0.0037"
}
audio_url is a permanent public link in our object storage, and can be fed straight into a digital human's (dh-avatar) audio_url.
The response("async": true)#
Returns a task id immediately, without blocking:
{ "id": "3a8e…", "model": "voice-tts-pro", "status": "pending", "characters": 1200, "created_at": "…" }
Then poll GET /v1/audio/speech/{id} (free):
{
"id": "3a8e…", "model": "voice-tts-pro", "status": "succeeded",
"characters": 1200, "duration_sec": "86.40",
"audio_url": "https://r2.dflop.top/audio-speech/…/xxx.mp3", "created_at": "…"
}
status has three states: pending / succeeded / failed. Failures are refunded in full automatically; you're only charged on success, and the audio again lands on a permanent link.
List tasks#
GET /v1/audio/speech (no id) — this account's synthesis tasks, newest first. Free.
Use it to recover the task id from an "async": true submit you didn't save.
| Query param | Default | Notes |
|---|---|---|
limit | 30 | 1–100 |
status | (all) | pending / succeeded / failed / all, comma-separated for multiple. An unrecognised value returns 400 |
The response is {"data": [ … ]}, and each item is field-for-field identical to GET /v1/audio/speech/{id} (id / model / status / characters / created_at, plus audio_url / duration_sec on success and error.message on failure).
curl "https://jiuye.zsopc.com/v1/audio/speech?limit=10" \
-H "Authorization: Bearer $PLATFORM_API_KEY"
/v1/audio/voices — voice cloning and management#
| Method | Path | Notes |
|---|---|---|
| POST | /v1/audio/voices | Clone a voice: {name, audio_url, async?} (a public reference-audio URL, 5 seconds to 3 minutes of clear speech). Billed per call (voice-clone-pro, 40.44 each) |
| GET | /v1/audio/voices | List this account's cloned voices plus the platform presets: {voices:[…], presets:[{id, name}]} |
| GET | /v1/audio/voices/{id} | Check one voice's status (pending / ready / failed) |
| DELETE | /v1/audio/voices/{id} | Delete a voice (our record of it) |
Cloning also supports "async": true: it returns {id, status:"pending"} immediately and you poll GET /v1/audio/voices/{id} until ready. By default it blocks until ready (tens of seconds to minutes) — which, as above, runs into the CDN's ~100-second ceiling, so new integrations should always use async. Failures are refunded in full automatically.
Pass the resulting voice id as voice on /v1/audio/speech to synthesise with it, or as the voice for a text-driven digital human (dh-avatar; see Digital human API). Before cloning a real person's voice, make sure you have their permission.
The platform also offers a set of shared voices (the presets in the GET /v1/audio/voices response) — pass one of their ids as voice with no cloning required.
POST /v1/transcripts/extract#
Short-video link → spoken script: paste a share link or share token from a short video and get back the video's spoken script plus metadata (title, cover, platform, length). The upstream detects the platform itself (Douyin, Kuaishou, Xiaohongshu, Bilibili, WeChat Channels and other major sites), so you don't specify a source.
The server blocks synchronously until extraction finishes (internally: create a task, then poll upstream — usually 5–40 seconds, at most about 55). Set a generous client read timeout (≥ 90 seconds recommended). The upstream's concurrency ceiling is low, so heavy parallel use queues up and slows down.
The request#
{
"url": "https://v.douyin.com/xxxxxx/ — or just paste the raw share token"
}
| Field | Notes |
|---|---|
url | Required. A short-video share link or raw share token (≤ 2000 characters). input is an equivalent alias. |
The response#
{
"model": "video-transcript",
"content": "the extracted spoken script…",
"title": "the original video title",
"cover": "https://… cover image URL",
"platform": "douyin",
"duration_sec": 42,
"origin_link": "https://… the original link echoed by upstream"
}
platform is the platform upstream identified (douyin, kuaishou and so on). content is the script itself; title, cover and duration_sec are supporting metadata and may be empty when the video has no such field.
curl#
curl -X POST https://jiuye.zsopc.com/v1/transcripts/extract \
-H "Authorization: Bearer $GPUSHARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://v.douyin.com/xxxxxx/"}'
How billing works#
- A flat per-call charge (
video-transcript, 20.22 each) against the account balance (shared by all keys). - Only successes are billed: an unparseable link, an unsupported video, an extraction timeout, or a script blocked by content safety are all free; only a clean, successful script costs you one call.
- Each call is recorded in usage and the call log as
unit_type=transcript(the response carriesx-gateway-trace; include it with the timestamp when reporting a problem).
Limits and errors#
- The
coverURL may be time-limited (roughly 24 hours) — download and store it yourself if you need it long-term. - Input is capped at 2000 characters, and the upstream's low concurrency ceiling means heavy parallel use queues.
- Errors use the normalised shape shared with every other endpoint: an invalid link or unsupported video → 400, an extraction timeout → 504, service quota temporarily exhausted → 503, an upstream connection failure → 502, insufficient balance → 402.
- To restrict a key to this capability only, put
video-transcriptin itsallowed_models(noallowed_modelsmeans every model the account can use).