Image / video / music APIs

/v1/images/generations and /v1/images/edits 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 — put the same sk-gpushare-* key in an Authorization: Bearer header (the universal form, used by every example on this page; x-api-key, x-goog-api-key and ?key= are equally supported); see Authentication. Everything bills against your account balance (shared by all keys), and an insufficient balance returns 402 quota_exceeded.

EndpointPurposeBilling
POST /v1/images/generationsText-to-image and image-to-image (synchronous, or "async": true for a task)per image
POST /v1/images/editsImage-to-image, accepting JSON or a multipart file upload (synchronous, or "async": true)per image (same price as above)
POST /v1/videos/generationsText-to-video and image-to-video (async task)per second
GET /v1/videos/generations/{id}Poll a video taskfree
GET /v1/videos/generationsList this account's video tasks (all statuses by default; ?limit= default 30, max 100; ?status= to filter)free
POST /v1/music/generationsAI music generation (Suno, async task)per generation (2 songs each)
GET /v1/music/generations/{id}Poll a music taskfree
GET /v1/music/generationsList this account's music tasks (same shape)free
POST /v1/audio/speechSpeech synthesis (synchronous, or "async": true for a task)per character
GET /v1/audio/speech/{id}Poll a speech taskfree
GET /v1/audio/speechList this account's speech tasks (same shape)free
POST /v1/transcripts/extractShort-video link → spoken script (synchronous)per call

Every response from these endpoints carries an x-gateway-trace header — including 4xx/5xx errors. Include it with the timestamp, the model and the full error body when reporting a problem, and paste it into logs.dflop.top to look the call up.


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://oemoemapi.dflop.top/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 extra Idempotency-Replayed: true header. 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-Key from a different sk-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://oemoemapi.dflop.top/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://oemoemapi.dflop.top/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",
  };
}
CaseResponse
Header absentBehaves exactly as before (no deduplication at all)
Malformed key400 invalid_idempotency_key
Same key, different body409 idempotency_key_reuse — use a fresh key
Same key, previous call still running409 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 retain409 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:

  1. Re-send the exact same curl (same Idempotency-Key) → you get the original task id straight back. See the section above.
  2. 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://oemoemapi.dflop.top/v1/videos/generations?limit=10" \
      -H "Authorization: Bearer $PLATFORM_API_KEY"
    
    ?status= filters, comma-separated for multiple: video accepts queued,running,succeeded,failed,expired,cancelled,all; music processing,succeeded,failed,expired,cancelled,all; speech pending,succeeded,failed,all. Pass ?status=succeeded for the pre-2026-07-31 default.
  3. 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 (image with "async": true, video, music, speech, avatar, voice) carry a task ID; calls with no task row behind them (chat, synchronous image, transcript) carry only a request ID.
    • The request ID is the value of the x-gateway-trace response header (present on every response, including 4xx/5xx).
    • ⚠️ An async task only becomes a ledger row once it reaches a terminal state and settles; a task still generating shows in the In progress strip at the top of the log page (with its task id and the reserved credits), or via the list endpoints in point 2. The log defaults to the last 30 days. See Reconciliation and the call log below for the exact semantics.

Reconciliation and the call log#

logs.dflop.top (sign in with your oem.dflop.top account) is the gateway's per-call ledger and the reference for reconciling against your own records. The semantics:

  • One row = the terminal outcome of one external call. Failed attempts produced by the gateway's automatic failover across upstream lanes are not separate rows, not billed and not counted as requests; they are folded into the terminal row's "internal attempts (not billed)" timeline (the row is tagged 换道 ×N).
  • Four statuses: success / error / interrupted (client disconnected or gateway timeout) / rejected. Rejected = refused by the gateway at submit time (bad parameters, unavailable model, insufficient balance, content block); cost is always 0, but the row is recorded so you can account for requests that were sent but never ran. total_requests on GET /api/v1/usage includes rejected rows and excludes internal attempts.
  • In progress: async tasks (image / video / music / speech) that have not reached a terminal state appear in the In progress strip at the top of the log page, with the task id, submit time and the reserved credits; on settlement the reservation is trued up to actual output and the task becomes a normal row.
  • Submitted / completed time: async tasks record both; synchronous calls record the completion time and derive the submit time from latency.
  • Result links: successful image / video / music / speech rows carry result_urls and result_expires_at (platform-hosted images are swept after 24 hours; video / music / speech links are long-lived or carry their own expiry). After expiry the log keeps only the link text.
  • Lookup by id: the search box matches both the task id and the request id (= the x-gateway-trace response header).
  • CSV export columns: id, created_at, key_name, key_prefix, model, status, error_code, error_message, input_tokens, output_tokens, cached_tokens, unit_count, unit_type, cost_usd, latency_ms, request_id, task_id, submitted_at, attempts_count, result_urls, result_expires_at. Multiple result_urls are joined with |; the cost_usd column name is historical — the value is in credits. One export is capped at 10,000 rows; past that the response carries x-truncated: true, so narrow the date range and export in slices.

POST /v1/images/generations#

OpenAI Images API-compatible shape, returned synchronously by default; add "async": true for submit + poll on long generations (see "Async mode" below).

Available models#

Model IDDisplay namePrice (per image)Notes
doubao-seedream-4-0-250828Seedream 4.011.73size ≥ 960×960
doubao-seedream-4-5-251128Seedream 4.514.96size must be ≥ 1920×1920, or upstream returns 400
doubao-seedream-5-0-260128Seedream 5.012.94size must be ≥ 1920×1920, or upstream returns 400
doubao-seedream-5-0-pro-260628Seedream 5.0 Pro17.79 for output ≤ 2.36 MP, 35.59 above thatsize ≥ 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-imageGrok Imagine (Image)28.31standard tier
grok-imagine-image-qualityGrok Imagine (Quality)28.31high-quality tier
gpt-image-2GPT-Image 223.86text-to-image and image-to-image (references are not charged). The upstream ignores size — put the aspect ratio in the prompt; measured latency 30–215 seconds. The older ids gpt-image-2-low/medium/high and tvod-gpt-image2-* resolve here as aliases
tvod-midjourney-v8.1Midjourney v8.140.44Every request returns exactly 4 images (a 2×2 grid); n does not control the count ⇒ 161.76 per request. size only sets the aspect ratio, never the pixel dimensions. Pick the output tier by appending --sd / --hd to the prompt
tvod-midjourney-v7Midjourney v732.35Same — fixed 4 images ⇒ 129.41 per request

size is 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). gpt-image-2 is the exception: its upstream never reads size — to control the frame, write "Aspect ratio: 16:9 (landscape)" into the prompt, which overrides size.

The two Midjourney entries differ from every other model on this endpoint in two ways — integrate against the table notes above:

  1. n does not control the image count. The upstream produces a 2×2 grid, so a request always returns 4 images (measured 2026-09-01: both n=1 and n=2 came back with 4). Pass n: 1. Billing settles on the number actually returned, so one v8.1 request costs 40.44 × 4 = 161.76 credits and your balance must cover that. The gateway's pre-flight check also gates on 4 images, so an insufficient balance is a 402 at submit time rather than an overdraft afterwards.
  2. size expresses an aspect ratio, not pixels. The gateway maps "WxH" onto the nearest supported frame (1:1, 16:9, 9:16, 4:3, 3:4) and hands that to the upstream; the actual pixel dimensions follow from the model and the output tier. Measured: 2048x2048 → four 1024x1024; 2048x1152 → four 1456x816; the same 2048x1152 with --hd → four 2944x1648. Do not treat size as a promise of exact resolution. A ratio that doesn't reduce to one of those five falls back to the model's default frame.

The output tier goes at the end of the prompt (--sd for the standard tier, --hd for the higher one), like any other Midjourney flag — the prompt is forwarded verbatim. Reference images and the advanced flags (--iw, --sref, …) have not been regression-tested on this channel and are not advertised as supported. Because a request takes a while, prefer the async mode below ("async": true plus polling).

The request#

{
  "model": "doubao-seedream-4-5-251128",
  "prompt": "A panda drinking tea in a bamboo grove, watercolour style",
  "size": "2048x2048",
  "n": 1
}
FieldRequiredNotes
modela Model ID from the table above
promptthe description
size"WxH", passed through upstream (mind each SKU's minimum)
nhow 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. ⚠️ Fixed-grid SKUs ignore n (Midjourney always returns 4): those gate on unit price × max(n, 4), so pass n: 1 — see the model-table notes above
imagereference image(s) for image-to-image: a string or an array, each item a public https URL or data:image/...;base64,.... Omit it for text-to-image. gpt-image-2, Seedream and nano-banana-2 take data URIs; nano-banana and nano-banana-pro take public URLs only
image_urlsequivalent spelling (array); both are merged and wire order is preserved (order carries meaning for multi-image composition)

Image-to-image (references)#

Same endpoint — pass image and it's image-to-image. Full examples, SDK usage, aspect-ratio control and the common pitfalls are in Image-to-image editing.

curl https://oemoemapi.dflop.top/v1/images/generations \
  -H "Authorization: Bearer $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 300 \
  -d '{
    "model": "gpt-image-2",
    "prompt": "Make the sofa blue, leave everything else unchanged. Aspect ratio: 3:2 (landscape)",
    "image": ["https://your-host.com/original.png"]
  }'

⚠️ When a reference is a URL, the server fetching it is the upstream's, not this gateway's — object storage inside China, intranet addresses and image hosts behind auth or bot protection come back as an upstream 400, Unable to download content from the provided URL. Send a data URI instead, or upload the raw file through /v1/images/edits below.

The response#

{
  "model": "doubao-seedream-4-5-251128",
  "created": 1765432100,
  "expires_at": 1765518500,
  "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. expires_at (a gateway extension) is when data[].url expires (Unix seconds); it appears only when the image is hosted by this platform (r2.dflop.top) — upstream pre-signed links carry no such field.

curl#

curl https://oemoemapi.dflop.top/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#

  • Returned image URLs are always temporary — most SKUs return an upstream pre-signed link (about 24 hours), while gpt-image-2 measurably returns a platform-hosted https://r2.dflop.top/gateway/images/ephemeral/<uuid>.png (swept after 24 hours; the top-level expires_at in the body is the exact expiry; images served through a relay lane are re-hosted on the same domain). Neither is permanent hosting — download and store them promptly
  • Synchronous by default. 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, but gpt-image-2 measurably takes 30–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)

Async mode (use it for long generations)#

Synchronous calls are bounded by a CDN non-streaming response limit of roughly 125 seconds. The gateway keeps the connection alive (past 90 seconds it commits the response headers and drips whitespace while it waits, so synchronous calls are no longer cut off by the CDN). The cost of that trick: a turn that fails after the 90-second mark cannot return a truthful HTTP status code — it comes back as 200 with an {"error": ...} body.

For clean failure semantics — or simply to avoid holding an HTTP connection open for minutes — add "async": true to the request body to switch to submit + poll:

# 1. Submit — returns immediately, does not wait for the image
curl https://oemoemapi.dflop.top/v1/images/generations \
  -H "Authorization: Bearer $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-image-2", "prompt": "a panda drinking tea in a bamboo forest", "async": true}'
# → {"id": "9f1e...", "status": "queued", "model": "gpt-image-2", "created_at": 1786000000}

# 2. Poll
curl https://oemoemapi.dflop.top/v1/images/generations/9f1e... \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

The polling contract is a single rule:

ResponseMeaning
202 + {"id","status","model","created_at"}Still running — keep polling (every 2–3 seconds is fine)
Any other status codeThis is the answer — byte-for-byte the same response a synchronous call would have produced (200 + data[] on success; the upstream's own status code and body on failure)
  • Bad parameters, unavailable models, insufficient balance and content blocks are still returned synchronously as truthful 4xx at submit time — you never get a task id for a request that was doomed
  • A task runs for at most 280 seconds (the whole channel-ladder budget), so budget ≥ 5 minutes of polling; a client-side timeout is not a failure — the task keeps running and settles as usual, so recover the result by id from the list endpoint below instead of resubmitting
  • Balance is reserved at submit for the worst case (per-image price × n plus reference-image cost) and settled against actual output; zero output is refunded in full
  • GET /v1/images/generations lists your own tasks (all statuses by default, newest first) — if you lose a task id, recover it here instead of resubmitting; parameters and fields are under "List tasks" below
  • /v1/images/edits supports it too: add "async": true to a JSON body, or an extra -F "async=true" field to multipart
  • Tasks interrupted by a deploy are refunded in full and marked failed — you are never charged twice

List tasks#

GET /v1/images/generations filters and paginates (since 2026-10-16); for reconciliation, pull the full range by submit time:

ParameterMeaning
statusqueued / running / succeeded / failed / in_flight (queued + running); default all
from / toFilter by submit time; accepts unix seconds, RFC 3339 or YYYY-MM-DD (to is exclusive)
limit1–100, default 30
cursorThe previous page's next_cursor; null means the end

Each item: id / status / model / endpoint (generations or edits) / created_at (submit) / started_at / completed_at (unix seconds) / unit_count (images actually produced) / cost (settled credits, 0 on failure) / error_code / request_id (= x-gateway-trace). succeeded items also carry result: {"urls": [...], "expires_at": <unix seconds|null>}urls are byte-identical to the polling response's data[].url, and expires_at appears only on platform-hosted links (24 hours).

curl "https://oemoemapi.dflop.top/v1/images/generations?status=succeeded&from=2026-08-30&to=2026-08-31&limit=100" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

The Nano Banana family (nano-banana 15.77, nano-banana-pro 54.19, nano-banana-2 16.18 per image) also runs through this endpoint and bills per image. nano-banana-2 returns b64_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/images/edits#

The other way into image-to-image. It exists so you can use the call shape your OpenAI SDK already speaks (client.images.edit(image=...) sends multipart), or upload raw local file bytes without base64-encoding them yourself.

Models, pricing, the channel ladder, Idempotency-Key, the response and the error shape are identical to /v1/images/generations — multipart is normalised into the same JSON envelope and continues down the same path. The one behavioural difference: a request with no reference is rejected with 400 here (on this endpoint a missing reference is a caller bug, and it will never silently degrade into text-to-image).

Two request shapes#

JSON (same shape as generations):

curl https://oemoemapi.dflop.top/v1/images/edits \
  -H "Authorization: Bearer $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 300 \
  -d '{
    "model": "gpt-image-2",
    "prompt": "Make the sofa blue, leave everything else unchanged",
    "image": ["data:image/png;base64,iVBORw0KGgo..."]
  }'

multipart/form-data (file field image, or image[] for several; other text fields pass through verbatim):

curl https://oemoemapi.dflop.top/v1/images/edits \
  -H "Authorization: Bearer $PLATFORM_API_KEY" \
  --max-time 300 \
  -F "model=gpt-image-2" \
  -F "prompt=Make the sofa blue, leave everything else unchanged" \
  -F "image[][email protected]" \
  -F "image[][email protected]"

With the OpenAI SDK#

from openai import OpenAI

client = OpenAI(
    api_key=PLATFORM_API_KEY,
    base_url="https://oemoemapi.dflop.top/v1",
    timeout=300.0,                       # gpt-image-2 measurably takes 30–215s
)

result = client.images.edit(
    model="gpt-image-2",
    image=open("original.png", "rb"),
    prompt="Make the sofa blue, leave everything else unchanged. Aspect ratio: 3:2 (landscape)",
)
print(result.data[0].url)

Limitations#

  • No image400 (the error tells you to use /v1/images/generations for text-to-image)
  • Request body limit 95 MB; base64 inflates size by roughly 33%, so real phone photos (~3 MB each) cap out around 20 images
  • Everything else (24h URL expiry, timeout budgets, error shape) matches /v1/images/generations
  • For fuller usage and how to choose a route, see Image-to-image editing

POST /v1/videos/generations#

An async task: submitting returns a task id immediately, and you poll until succeeded.

Available models#

Model IDDisplay namePrice (per second)Notes
doubao-seedance-1-0-pro-fast-251015Seedance 1.0 Pro Fast32.35
doubao-seedance-1-0-pro-250528Seedance 1.0 Pro60.66
doubao-seedance-1-5-pro-251215Seedance 1.5 Pro72.79
doubao-seedance-2-0-fast-260128Seedance 2.0 Fast48.53
doubao-seedance-2-0-260128Seedance 2.088.97
doubao-seedance-2.0Seedance 2.0by resolution: 480p 27.72144 / 720p 59.616 / 1080p 148.716supports real-person photos on camera; reference images are moderated and registered automatically. The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
doubao-seedance-2.0-fastSeedance 2.0 Fastby resolution: 480p 22.29768 / 720p 47.952 / 1080p 107.892The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
doubao-seedance-2.0-miniSeedance 2.0 Miniby resolution: 480p 13.86072 / 720p 29.808lightweight tier; 480p/720p only, 4–15 seconds. The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
doubao-seedance-2.5Seedance 2.5by resolution: 480p 40.3515 / 720p 90.72 / 1080p 224.532multimodal references from images (≤30), video and audio; can generate sound; 4–30 seconds. The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
doubao-seedance-2.0-liteSeedance 2.0 Liteby resolution: 720p 59.616 / 1080p 148.716value tier; resolution must be passed explicitly (720p/1080p only). The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
doubao-seedance-2.0-fast-liteSeedance 2.0 Fast Liteby resolution: 720p 47.952 / 1080p 107.892as above, lower latency. The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
doubao-seedance-2.0-mini-liteSeedance 2.0 Mini Liteby resolution: 720p 29.808 / 1080p 74.358as above, cheapest for lightweight work. The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
doubao-seedance-2.5-liteSeedance 2.5 Liteby resolution: 720p 42.8515 / 1080p 95.72as above; same capabilities as doubao-seedance-2.5 (4–30 seconds, multimodal references, sound). Its 1080p tier is far cheaper than the full-price card (95.72 vs 224.532) — same delivered resolution, different cost structure. The whole family is billed by token (formula and minimum usage in the billing section below; resolution is required when a reference video is present, otherwise 400)
grok-imagine-videoGrok Imagine Video283.08text-to-video and image-to-video
grok-imagine-video-1.5-previewGrok Imagine Video 1.5586.38image-to-video only (upstream 400s without a reference image)
dh-avatarDigital human videoflat 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-realmanSmart edit · talking head4.04/second (of finished video)a talking-head source video + a template → a finished cut with title, subtitles, name bar and music
clip-mixcutSmart edit · asset mixcut4.04/second (of finished video)narration audio + image/video assets + a template → a subtitled, packaged cut
clip-newsSmart edit · news brief2.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
}
FieldRequiredNotes
modela Model ID from the table above (grok SKUs use the same shape; the gateway translates)
contentan array: {type:"text", text} is mandatory; for image-to-video append {type:"image_url", image_url:{url:"https://..."}}
durationseconds. Omitted, we hold 12 seconds' worth (Seedance's ceiling) and settle on the real length — pass it explicitly
ratio / resolution / watermarkpassed through upstream (Seedance's own semantics). ⚠️ resolution is required on the four 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_modeonly 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 custom duration); video_mode: "extend" switches to continuation (carrying on from the last frame for duration seconds, 2–10). For image-to-video the gateway deliberately withholds ratio so 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
}
FieldNotes
image_url.urlmust 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_authOptional 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.
resolutionThe real-person vocabulary: doubao-seedance-2.0 and -fast take 480p/720p/1080p, -mini only 480p/720p. A value outside that vocabulary returns 400 and the error message lists the tiers that SKU accepts. Omitted means 720p: upstream delivers 720p and we bill the official 720p token rate, i.e. exactly what an explicit 720p costs.

Multimodal references (Seedance 2.0 only): beyond a single first-frame image, content[] can carry reference media items tagged with a role{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). duration only sizes the billing hold, and settlement uses upstream's actual seconds.

FieldRequiredNotes
avatarthe avatar id (created once under "Clone avatar" in the app, then reusable)
audio_urlone of the twodriving audio (public URL, mp3/wav) — makes the avatar speak it
voice + textone of the twotext-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
titlea name for the piece (≤20 characters)

Things to note:

  • input media URLs must be publicly reachable (anything you've uploaded to our r2.dflop.top works 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" }
}
FieldApplies toNotes
style_idall ✓template id (from the platform's smart-edit template library)
source_video_urlrealman ✓the talking-head source video (public URL)
audio_urlmixcut ✓the narration audio (public URL)
materialsmixcut/news ✓, optional for realmanan array of {type:"image"|"video", file_url, sound_switch?}, at most 10
titlenews ✓, optional elsewherethe piece's or story's headline
durationnewstarget length in seconds, 5–300 (clamped if out of range); a billing hint only for realman/mixcut
material_compositionnewsrandom or order; random by default
preprocessrealmanasset pre-processing: roughCut or sliceMerge
bgmall{mode:"auto"|"none"|"custom", url?, volume?}; follows the template by default
cover_urlalla custom first-frame cover (public image URL)
introduce_cardallthe name bar, {name, description}
languageallsubtitle 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 duration explicitly (for a long source video, say) holds that instead. Insufficient balance returns 402, and failures or expiry are refunded in full.

Template ids: style_id comes 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).

MediumFormatSizeResolutionLength
Talking-head source video source_video_urlmp4 / 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 sidecounts 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.urlmp3 / wav / m4a≤ 120MB≤ 5 minutes
First-frame cover cover_urljpg / 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-news output length is set by duration (5–300s); clip-realman and clip-mixcut follow the source video and the narration audio respectively.

Polling#

curl https://oemoemapi.dflop.top/v1/videos/generations/$TASK_ID \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

status runs queuedrunningsucceeded / 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,
  "updated_at": 1765432460,
  "ratio": "16:9",
  "resolution": "1080p",
  "duration": 5,
  "generate_audio": true,
  "seed": 33608,
  "output_format": "mp4",
  "service_tier": "default",
  "execution_expires_after": 172800,
  "draft": false,
  "priority": 0,
  "framespersecond": 24,
  "content": {
    "video_url": "https://...",
    "last_frame_url": "https://..."
  },
  "usage": { "completion_tokens": 411300, "total_tokens": 411300 },
  "video_url": "https://...",
  "expires_at": 1765435700,
  "duration_sec": 5.0
}

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.

Response fields#

FieldPresent whenNotes
idalwaysTask id
statusalwaysqueued / running / succeeded / failed / expired / cancelled
modelalwaysThe submitted Model ID (canonicalised)
created_atalwaysTask creation time, Unix seconds
updated_atalwaysWhen the status last changed, Unix seconds. For terminal rows this is when the task settled; while in flight it falls back to created_at (we don't stamp the intermediate queued/running transitions). Always ≥ created_at and never in the future
seed / ratio / generate_audio / output_format / service_tier / execution_expires_after / draft / prioritySeedance SKUs, after the first pollThe value upstream actually used. Use seed to reproduce the same clip; the rest let you see what upstream picked even when you didn't specify it.<br>⚠️ Three preconditions, all required: (1) the task has been polled at least once (a freshly submitted task has none yet); (2) seed and ratio only appear once succeeded (they're results), the rest from the first poll on; (3) the task actually ran on an upstream that reports them — when the primary channel is unavailable a task may be served by a fallback channel, and these keys are then absent entirely, falling back to the row below. They are also cleared and re-accumulated if the system re-routes a task to another channel (the old channel's observation is void). Write your parser to tolerate absence — don't assert these are always there
resolution / frames / safety_identifieronly if you sent themThe value you specified in the create request, echoed back. Keys you didn't send are absent — we don't invent upstream defaults on your behalf. Request-side "you decide" sentinels are likewise not echoed: seed: -1 (random) and ratio: "adaptive" (auto-fit).<br>⚠️ resolution is deliberately echoed from the tier you ordered rather than taken from upstream: some SKUs generate one tier lower internally and upscale back to the delivered tier, so reading upstream would show you a lower resolution than you paid for
input_video_duration_secsucceeded only, on SKUs billed as input + outputUpstream-reported input reference video seconds (fractional). 0 = there genuinely was no reference video. ⚠️ An absent key means one of two things: this SKU doesn't bill input seconds, or it does but upstream didn't report it this round — the two are indistinguishable on the wire, so don't read absence as "no reference video". Gateway extension, not present upstream
durationsucceeded onlyDelivered length in whole seconds. This is the delivered duration (same source as duration_sec, rounded), not the duration you submitted. Equal for Seedance; they differ on SKUs that estimate length from a script (digital human text-driven), where this field is authoritative
contentsucceeded onlyOutput block. video_url = the finished asset; last_frame_url = the last frame (see below)
content.last_frame_urlsent return_last_frame: true on create, succeeded, and not a two-stage SKUPNG of the video's final frame, same dimensions as the video, no watermark. Use it to chain long videos: feed it as the first frame of the next task. Already copied into our object storage, so the link is permanent (the upstream original expires in 24h) ⚠️ Never returned on two-stage SKUs (the cards that generate downscaled and then upscale): upstream only provides a last frame for stage one, and its dimensions are the downscaled ones — they wouldn't match the video you received, which would break the chaining use case.
framespersecondonly when upstream reports itFrame rate of the finished video. Absent when upstream didn't report one (it does not mean 24)
usagesucceeded, and only when upstream reports token usage{completion_tokens, total_tokens}. Video models don't count input tokens, so the two are equal. This is upstream's usage figure, not this platform's billing basis — see below
video_urlsucceeded onlySame value as content.video_url, flattened
expires_atsucceeded onlyWhen video_url expires, Unix seconds
duration_secsucceeded onlySame as duration but keeps fractional precision. A gateway extension; Ark has no such field
output_filessucceeded, subtitle SKUs onlyOne download link per language
progressqueued/running, and only when upstream reports itInteger 0–100. An absent field means the model has no progress data — not 0%
errorfailed/expired/cancelled only{code, message}

How usage relates to billing#

usage.completion_tokens is passed through verbatim from upstream: if upstream reports it, we emit it, regardless of how that SKU is priced here. Seeing a token count does not mean the SKU is billed per token.

Each model's pricing basis is listed in Models & pricing and is one of three things:

  • Per second (most video SKUs): charge = unit price × delivered seconds; reconcile against duration_sec. usage is informational here and plays no part in pricing.
  • Per second, with input video billed too (Alibaba Bailian wan3.0-video and wan3.0-video-prime): charge = unit price × (input video seconds + delivered seconds). This is the upstream's own basis — its pricing page states that both the input and the output video are billed by video second, that billed duration = input duration + output duration, and the rate column is literally headed "input and output unit price" (one rate covering both legs — not a new pricing dimension).
    • Reconcile against the top-level input_video_duration_sec (our extension field; fractional seconds; emitted only on a successful terminal state for these two cards, and emitted as 0 when the round carried no reference video, so you can tell "none" from "not reported"). Billed seconds = input_video_duration_sec + duration_sec.
    • duration_sec still means only the delivered clip's length and excludes the input — do not use it as the billed duration.
    • Reference-video limits come from upstream: [1,15] seconds each, 15 seconds total. We hold against that ceiling at submit time and refund the difference once upstream reports the real input duration; if upstream never reports it, the round settles at the ceiling.
    • Text-to-video and image-to-video rounds are byte-for-byte identical to the first basis above (input video seconds are 0). Image input is free upstream, and we don't charge for it either.
  • Per token (the whole Volcano Seedance family — both the 2.0 and 2.5 series): charge = token rate × usage.completion_tokens; reconcile against that. The four Lite cards add a second leg billed per delivered second — see below.
The Seedance billing formula (identical to upstream)

The whole Volcano Seedance family (2.0 series and 2.5 series, -lite cards included) is billed by token, using the same formula the upstream publishes in Volcano Ark — Model pricing:

charge     = token rate x token usage
token usage = (input video duration + output video duration) x output width x output height x frame rate / 1024
  • The frame rate is fixed at 24 (it is not a request parameter).
  • Width and height come from the "pixel size per aspect ratio" table under the ratio field of Create a video generation task, looked up by resolution x ratio. ⚠️ The 480p row differs between generations: Seedance 2.5 is 854x480, the 2.0 series is 864x496.
  • The authoritative usage is usage.completion_tokens in the response; the formula above is an estimate.
  • Minimum usage: when the input contains video, the input duration counts as at least 4 seconds — a 2-second reference video costs exactly the same as a 4-second one.
  • Reference images and the text prompt cost no tokens (upstream reports prompt_tokens: 0); extra reference images are free.
  • Reference audio is not part of the formula — the official formula has no audio term. It does not change the basis either: every turn on this family is token-billed.
  • Token rates are graded on two axes: output resolution x whether the input contains video. Per-tier numbers are in Models & pricing. The "with video input" rate is cheaper because it multiplies more seconds.
  • The four Lite cards bill two legs added together (doubao-seedance-2.0-lite / -fast-lite / -mini-lite / -2.5-lite, which generate one tier down and then upscale): token rate x tokens + upscale rate x delivered seconds (2.5/s at 720p, 5/s at 1080p). The second leg is charged on delivered seconds only — your reference video is never upscaled and you never pay for upscaling it. ⚠️ Their tokens are produced at the downgraded generation resolution (720p delivery generates at 480p, 1080p delivery at 720p), while the token-rate key uses the delivery tier.
How much is held

The hold at submit time is that same formula evaluated at its worst case, not "unit price x seconds":

  • the frame is the largest one at that resolution (the server cannot know which frame the model will pick — upstream states explicitly that ratio may be ignored when first/last frames, reference images or reference video are supplied);
  • when a reference video is present, the input duration is filled to the official total cap: 15 s for the Seedance 2.0 series, 30 s for Seedance 2.5.

Settlement then uses the real usage.completion_tokens reported by upstream and refunds the rest. Insufficient balance returns 402 at submit time with nothing charged.

  • ⚠️ resolution becomes required when a reference video is present; omitting it returns 400. Token rates are graded by delivery tier, so without a tier there is no price. Requests without a reference video are unaffected and are billed at the 720p tier when resolution is omitted (see the resolution notes above).
  • 2k / 4k were retired on 2026-09-11 — they were served by a different upstream leg, and that leg is gone. Passing either value now returns 400.

Under either basis, the authoritative charge is the record for that request on the console's usage page.

Alignment with Volcengine Ark#

For the Seedance SKUs, request and response fields follow Volcengine Ark's create / query video generation task contract, so code written against Ark's docs or official SDK can be pointed here unchanged. Six differences to know about:

  • The asset URL appears twice: content.video_url (Ark's shape) and top-level video_url (this gateway's original shape) are always emitted together with the same value. Read either.
  • Task ids are this platform's own UUIDs (e.g. 9f2c8a1e-…), not Ark's cgt--prefixed format. Store the id as an opaque string; don't validate its prefix or length.
  • Parameter fields are now "what upstream picked" (since 2026-09-13). seed / ratio / generate_audio / output_format / service_tier / execution_expires_after / draft / priority come from the upstream response, matching Ark's semantics — you see what upstream actually used even if you didn't specify it. When upstream doesn't report them we fall back to echoing what you sent, still skipping "upstream decides" sentinels like adaptive / -1. "Upstream doesn't report them" isn't limited to non-Seedance SKUs — a Seedance task may be served by a fallback channel when the primary is unavailable, and that leg doesn't return these fields, so two calls on the same model can differ. Write your parser to tolerate absence.
  • resolution is the exception: it is always echoed from the tier you ordered, never taken from upstream. Some SKUs generate one tier lower internally and upscale back, so reading upstream would show a lower resolution than you paid for.
  • Gateway extensions: expires_at / duration_sec / input_video_duration_sec / output_files / progress don't exist upstream. The extra keys are harmless to an Ark-shaped parser.
  • Ark fields not yet provided: tools (tools actually used), usage.tool_usage (tool call counts), and draft_task_id (the source task id when promoting a Draft). The request side forwards them verbatim, but you won't see them in the response.

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 paramDefaultNotes
limit301–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=succeeded for 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,
      "content": { "video_url": "https://..." },
      "video_url": "https://...",
      "expires_at": 1786094800
    }
  ]
}
FieldPresent whenNotes
idalwaysThe task id — the same value GET /v1/videos/generations/{id} takes
statusalwaysSame vocabulary as the polling endpoint
modelalwaysThe submitted Model ID (canonicalised)
created_atalwaysSubmit time, Unix seconds
progressqueued/running, and only when upstream reports itInteger 0–100. An absent field means the model has no progress data — not 0%
contentsucceeded only{video_url}, the same shape as the polling endpoint. ⚠️ Not necessarily the same URL though: this endpoint prefers a permanent public URL while the polling endpoint returns a presigned one. Both point at the same asset — don't compare or de-duplicate them as strings
video_urlsucceeded onlySame value as content.video_url. A presigned link valid for 7 days; calling this endpoint again re-signs an expired one
expires_atsucceeded onlyWhen video_url expires, Unix seconds
output_filessucceeded, subtitle SKUs onlyOne download link per language
errorfailed/expired/cancelled only{code, message}

This endpoint is a gateway extension (Ark has no list endpoint). Each entry also carries card fields — mode (t2v/i2v), prompt, ratio, resolution, duration — so a "generation history" UI can render straight from it. Note these are all reconstructed from the create request, which is a different basis from the polling endpoint: duration is the length you ordered (the poll returns the delivered length), and ratio is the value you sent (on Seedance the poll returns what upstream actually used). For accurate finished-video attributes, use the polling endpoint.

import requests
r = requests.get(
    "https://oemoemapi.dflop.top/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 × duration is held against the balance (12 seconds if duration is 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=succeeded for 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_url is a pre-signed link valid for 7 days (expires_at is 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 IDDisplay namePrice (per generation)
suno-v3.5Suno V3.519.41
suno-v4Suno V419.41
suno-v4.5Suno V4.519.41
suno-v5Suno V519.41
suno-v5.5Suno V5.5 (latest)19.41

The request#

curl https://oemoemapi.dflop.top/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"
  }'
FieldTypeNotes
modelstringrequired; any id from the table above
promptstringinspiration mode (≤200 characters): the AI writes the lyrics, picks a title and sings
lyricsstringyour own lyrics (≤3000 characters); passing them switches to custom mode and prompt is ignored
titlestringsong title (custom mode)
tagsstringstyle, e.g. "synthwave, female vocal"
negative_tagsstringstyles to avoid
instrumentalboolinstrumental 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://oemoemapi.dflop.top/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 paramDefaultNotes
limit301–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://oemoemapi.dflop.top/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_url as 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": true to 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 paramDefaultNotes
limit301–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://oemoemapi.dflop.top/v1/audio/speech?limit=10" \
  -H "Authorization: Bearer $PLATFORM_API_KEY"

/v1/audio/voices — voice cloning and management#

MethodPathNotes
POST/v1/audio/voicesClone 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/voicesList 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"
}
FieldNotes
urlRequired. 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://oemoemapi.dflop.top/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 carries x-gateway-trace; include it with the timestamp when reporting a problem).

Limits and errors#

  • The cover URL 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-transcript in its allowed_models (no allowed_models means every model the account can use).