Generations API — every async image, video and audio route
One document for every generation route on api.sociaro.com. All of them work the same way: you
submit a job, you get an id back immediately, and you poll for the result. Nothing here holds a
connection open while a model renders.
This page is the /v1 family, except where a row says /v2. One model —
sociaro/minimax-h3-mini — is served only by /v2/jobs, which shares that submit-and-poll shape and
nothing else: its own envelope, its own statuses, its own result field. It is listed here because it
is a generation model you can call, and its field list is as authoritative as the rest; for how to
call it, read Jobs API.
- Base URL:
https://api.sociaro.com - Auth:
Authorization: Bearer <your-api-key>on submit and on poll. POST /v1/videosserves no model in this document and answers404witherror.code: "wrong_endpoint", whose message names the route you actually want. It is the single most common wrong turn here — an OpenAI-style video route is what an agent guesses first. Use the table below.- Likewise, if you are guessing from
mode: video_generationon/model/info: that field says what a model produces, not where to send it./v1/modelsreturns only the OpenAI shape (id,object,created,owned_by), carries no routing information at all, and itsowned_byis always the literalopenaino matter who made the model — do not read a vendor out of it.
Which endpoint for which model
Generated from the live routing tables — if a model is in /v1/models, it is in this list.
| model id | where you send it |
|---|---|
spicy/face-swap |
submit POST /v1/spicy/generations · poll GET /v1/spicy/generations/{job_id} |
spicy/qwen-image-edit |
⤴ same as above |
spicy/seedance-2-0 |
⤴ same as above |
spicy/seedance-2-0-mini |
⤴ same as above |
spicy/seedance-2-5 |
⤴ same as above |
spicy/seedream-5-pro |
⤴ same as above |
spicy/wan2.2-i2v |
⤴ same as above |
spicy/wan2.7-i2v |
⤴ same as above |
spicy/z-image |
⤴ same as above |
alibaba/happyhorse-1-1-video |
submit POST /v1/alibaba/generations · poll GET /v1/alibaba/generations/{job_id} |
alibaba/happyhorse-1-1-video-i2v |
⤴ same as above |
alibaba/happyhorse-1-1-video-ref |
⤴ same as above |
alibaba/happyhorse-video-edit |
⤴ same as above |
alibaba/wan-3-video |
⤴ same as above |
alibaba/qwen-image-3 |
⤴ same as above |
alibaba/qwen-image-3-pro |
⤴ same as above |
alibaba/wan-2-7-image |
⤴ same as above |
alibaba/wan-2-7-image-pro |
⤴ same as above |
bytedance/seedance-2.0 |
submit POST /v1/byteplus/contents/generations/tasks · poll GET .../tasks/{id} |
bytedance/seedance-2.0-fast |
⤴ same as above |
bytedance/seedance-2.0-mini |
⤴ same as above |
bytedance/seedance-2.5 |
⤴ same as above |
bytedance/seedream-4-0 |
submit POST /v1/byteplus/images/async · poll GET .../images/async/{job_id} (or sync POST /v1/byteplus/images/generations) — same route as its native id seedream-4-0-250828 below; either spelling works |
bytedance/seedream-4-5 |
⤴ same as above |
bytedance/seedream-5-lite |
⤴ same as above |
bytedance/seedream-5-pro |
⤴ same as above |
dola-seedream-5-0-pro-260628 |
⤴ same as above |
seedream-4-0-250828 |
⤴ same as above |
seedream-4-5-251128 |
⤴ same as above |
seedream-5-0-260128 |
⤴ same as above |
seedream-5-0-lite-260128 |
⤴ same as above |
black-forest-labs/flux-2-pro |
submit POST /v1/images/async with your own webhook — we call you back, there is no poll |
black-forest-labs/flux-2-pro-2k |
⤴ same as above |
black-forest-labs/flux-2-pro-edit |
⤴ same as above |
google/nano-banana |
⤴ same as above |
google/nano-banana-2 |
⤴ same as above |
google/nano-banana-2-2k |
⤴ same as above |
google/nano-banana-2-4k |
⤴ same as above |
google/nano-banana-2-lite |
⤴ same as above |
google/nano-banana-edit |
⤴ same as above |
google/nano-banana-pro |
⤴ same as above |
google/nano-banana-pro-2k |
⤴ same as above |
google/nano-banana-pro-4k |
⤴ same as above |
openai/gpt-image-1-5 |
⤴ same as above |
openai/gpt-image-1-5-edit |
⤴ same as above |
openai/gpt-image-2 |
⤴ same as above |
openai/gpt-image-2-2k |
⤴ same as above |
openai/gpt-image-2-4k |
⤴ same as above |
openai/gpt-image-2-edit |
⤴ same as above |
xai/grok-imagine |
⤴ same as above |
xai/grok-imagine-edit |
⤴ same as above |
sociaro/minimax-h3-mini |
submit POST /v2/jobs · poll GET /v2/jobs/{id} — the only model here that is not on a /v1 route. /v2 has its own envelope — see Jobs API — and the field list below is what goes in that request's input |
The capability is in the MODEL ID, not in the request
This is the single thing that costs integrators the most time. Sibling ids are different models with
different accepted fields, and the shortest, most obvious-looking name in a family is usually the
text-only one. Sending an image field to it is rejected as unknown field(s) — and no amount of guessing
at field names will help, because the field does not belong to that model.
HappyHorse is the clearest example. Four ids, one family:
| model id | what it does | the input field |
|---|---|---|
alibaba/happyhorse-1-1-video |
text → video | none — this is the text-only one |
alibaba/happyhorse-1-1-video-i2v |
photo → video (this is "animate") | first_frame_url — one public https url |
alibaba/happyhorse-1-1-video-ref |
up to 9 reference images → video | reference_image_urls — array of urls |
alibaba/happyhorse-video-edit |
existing clip → edited clip | video_url (one clip) plus up to 5 reference_image_urls |
So for photo-to-video you want -i2v with first_frame_url. Reaching for alibaba/happyhorse-1-1-video
and looking for an image parameter is a dead end by design: it has none, and image, image_url,
first_frame, content, input_reference and every other spelling all come back unknown field(s)
because that model takes text only.
No HappyHorse model takes a last frame, and each takes exactly ONE kind of input:
| model | the only media type it accepts |
|---|---|
-i2v |
first_frame — a last_frame is rejected: Input should be 'first_frame' |
-ref |
reference_image — video is rejected: Input should be 'reference_image' |
-video-edit |
video (exactly one) plus up to 5 reference_image — reference_video is refused |
base -video |
none — it silently ignores any media you send (see below) |
The vendor's own API reference lists first_frame, last_frame, reference_image, reference_video
and reference_audio together, which reads as if every HappyHorse model took all of them. Those types
are real — but they span the vendor's whole video-generation API, not one model. On HappyHorse each
validator allows exactly one, as above; last_frame in particular belongs to wan2.7-i2v, a different
model. Every line in the table is from probing the live endpoint, and note that this vendor's submit
returns 200 regardless — the rejection only ever appears on the poll, so a 200 from it proves
nothing at all.
If you need a start AND an end frame, use spicy/wan2.2-i2v, which has a real last_image field.
Two things about these models that are easy to miss
-video-edit is meant to take a clip AND images together. Its headline use is "put the sweater from
this image onto the character in this video" — one video_url plus up to five reference_image_urls.
Sending only the clip works and edits it from the prompt alone, so the pairing is easy to overlook.
(reference_video_urls still works as the old name for the clip; video_url is the one to use.)
On -ref, the ORDER of reference_image_urls is part of the prompt. The Nth url is what your prompt
means by [Image N], up to 9 of them. From the vendor's own example:
{ "model": "alibaba/happyhorse-1-1-video-ref",
"prompt": "A woman in the red qipao from [Image 1] unfolds the fan from [Image 2] while the tassel earrings from [Image 3] sway",
"reference_image_urls": ["https://…/girl.jpg", "https://…/fan.jpg", "https://…/earrings.jpg"],
"resolution": "720p", "aspect_ratio": "16:9", "duration": 5 }
Re-order that array and the subjects swap. Nothing in the gateway sorts or de-duplicates it — the order you send is the order the model reads.
Why the base -video model is the dangerous one. Send it a first_frame and the vendor answers
SUCCEEDED — with a clip rendered from your prompt alone, your image ignored, and the job billed. It does
not even fetch the url. This gateway does not let that happen: the base model declares no url fields, so
we reject the request instead of letting you pay for a clip that ignored your input.
How to discover a model's accepted fields, without guessing
curl -sS https://api.sociaro.com/model/info -H "Authorization: Bearer $KEY" \
| jq -r '.data[] | select(.model_name=="alibaba/happyhorse-1-1-video-i2v")
| .model_info.supported_openai_params'
# -> ["prompt","first_frame_url","resolution","aspect_ratio","duration",
# "negative_prompt","seed","prompt_extend","watermark","audio_url"]
supported_openai_params is the authoritative list of field names this gateway accepts for that
model, and the routes that check field names reject anything outside it with unknown field(s): [...]
naming the offender. Read it before writing a request. The four alibaba/* image ids do no such
check — see the exception under the table below — so there the list is what we support rather than
what we enforce. One exception on the Seedance models: frames and camera_fixed
are not listed, because the generator refuses both on every Seedance model we serve (measured
2026-10-05; the vendor documents camera_fixed for Seedance 1.x only), yet both still pass our
field-name check, so they reach the generator, which answers with its own message instead of
unknown field(s). Note
what the list does NOT tell you: types, ranges, defaults or which fields are required — those are on
each model's own page under Models.
Every model, every field
The same lists, in one place, so you do not have to call /model/info to start. Anything outside a
model's row comes back 400 unknown field(s): [...] — with two exceptions, and they are different in
kind. On Seedance, frames and camera_fixed pass our check and are refused by the GENERATOR, in its
own words (see above) — still a refusal, just not ours. The second is not a refusal at all:
the four alibaba/* image ids have no field allowlist, so everything
you send other than prompt and the reference-image urls is forwarded to the generator untouched,
deliberately, so a parameter we have never heard of is the generator's to accept or refuse rather
than ours to guess at. Their rows are what we have measured and will support — a field outside one
may still work, it is simply not something we promise. enable_sequential on
alibaba/wan-2-7-image is the known case. What this table does NOT give you is types, ranges,
defaults and which fields are REQUIRED — for the nine spicy/* models those are in
each model's own page under Models; for the rest, send the field and read the 400, which names the problem.
| model id | makes | every field it accepts |
|---|---|---|
alibaba/happyhorse-1-1-video |
video | prompt, resolution, aspect_ratio, duration, negative_prompt, seed, prompt_extend, watermark, audio_url |
alibaba/happyhorse-1-1-video-i2v |
video | prompt, first_frame_url, resolution, aspect_ratio, duration, negative_prompt, seed, prompt_extend, watermark, audio_url |
alibaba/happyhorse-1-1-video-ref |
video | prompt, reference_image_urls, resolution, aspect_ratio, duration, negative_prompt, seed, prompt_extend, watermark, audio_url |
alibaba/happyhorse-video-edit |
video | prompt, video_url, reference_image_urls, reference_video_urls, resolution, aspect_ratio, duration, negative_prompt, seed, prompt_extend, watermark, audio_url |
alibaba/qwen-image-3 |
image | prompt, size, n, watermark, negative_prompt, seed, prompt_extend, prompt_extend_mode, enable_thinking, image_urls |
alibaba/qwen-image-3-pro |
image | prompt, size, n, watermark, negative_prompt, seed, prompt_extend, prompt_extend_mode, enable_thinking, image_urls |
alibaba/wan-2-7-image |
image | prompt, size, n, watermark, thinking_mode |
alibaba/wan-2-7-image-pro |
image | prompt, size, n, watermark, thinking_mode |
alibaba/wan-3-video |
video | prompt, first_frame_url, last_frame_url, reference_image_urls, reference_video_urls, reference_audio_urls, resolution, aspect_ratio, duration, negative_prompt, seed, prompt_extend, watermark, audio |
black-forest-labs/flux-2-pro |
image | prompt, nsfw_checker |
black-forest-labs/flux-2-pro-2k |
image | prompt, nsfw_checker |
black-forest-labs/flux-2-pro-edit |
image | prompt, image_urls, nsfw_checker |
bytedance/seedance-2.0 |
video | content, ratio, duration, resolution, output_format, generate_audio, watermark, seed, bitrate_mode, return_last_frame |
bytedance/seedance-2.0-fast |
video | content, ratio, duration, resolution, output_format, generate_audio, watermark, seed, bitrate_mode, return_last_frame |
bytedance/seedance-2.0-mini |
video | content, ratio, duration, resolution, output_format, generate_audio, watermark, seed, bitrate_mode, return_last_frame |
bytedance/seedance-2.5 |
video | content, ratio, duration, resolution, output_format, generate_audio, watermark, seed, bitrate_mode, return_last_frame, omni_reference_task_type, draft |
bytedance/seedream-4-0 |
image | prompt, image, size, output_format, response_format, watermark, seed, sequential_image_generation, sequential_image_generation_options |
bytedance/seedream-4-5 |
image | prompt, image, size, output_format, response_format, watermark, seed, sequential_image_generation, sequential_image_generation_options |
bytedance/seedream-5-lite |
image | prompt, image, size, output_format, response_format, watermark, seed, sequential_image_generation, sequential_image_generation_options |
bytedance/seedream-5-pro |
image | prompt, image, size, output_format, response_format, watermark, seed, layer_decomposition, background |
dola-seedream-5-0-pro-260628 (vendor-native id; same model as bytedance/seedream-5-pro) |
image | prompt, image, size, output_format, response_format, watermark, seed, layer_decomposition, background |
google/nano-banana |
image | prompt, nsfw_checker |
google/nano-banana-2 |
image | prompt |
google/nano-banana-2-2k |
image | prompt |
google/nano-banana-2-4k |
image | prompt |
google/nano-banana-2-lite |
image | prompt |
google/nano-banana-edit |
image | prompt, image_urls |
google/nano-banana-pro |
image | prompt |
google/nano-banana-pro-2k |
image | prompt |
google/nano-banana-pro-4k |
image | prompt |
openai/gpt-image-1-5 |
image | prompt |
openai/gpt-image-1-5-edit |
image | prompt, image_urls |
openai/gpt-image-2 |
image | prompt |
openai/gpt-image-2-2k |
image | prompt |
openai/gpt-image-2-4k |
image | prompt |
openai/gpt-image-2-edit |
image | prompt, image_urls |
seedream-4-0-250828 (vendor-native id; same model as bytedance/seedream-4-0) |
image | prompt, image, size, output_format, response_format, watermark, seed, sequential_image_generation, sequential_image_generation_options |
seedream-4-5-251128 (vendor-native id; same model as bytedance/seedream-4-5) |
image | prompt, image, size, output_format, response_format, watermark, seed, sequential_image_generation, sequential_image_generation_options |
seedream-5-0-260128 (vendor-native id; same model as bytedance/seedream-5-lite) |
image | prompt, image, size, output_format, response_format, watermark, seed, sequential_image_generation, sequential_image_generation_options |
seedream-5-0-lite-260128 (vendor-native id; same model as bytedance/seedream-5-lite) |
image | prompt, image, size, output_format, response_format, watermark, seed, sequential_image_generation, sequential_image_generation_options |
sociaro/minimax-h3-mini (/v2 only; content and the flat form are alternatives — send content, or prompt with the reference_*_urls, never both) |
video | content, prompt, reference_image_urls, reference_video_urls, reference_audio_urls, duration, resolution, ratio, seed |
spicy/face-swap |
image | image, face_image, seed |
spicy/qwen-image-edit |
image | image, prompt, seed |
spicy/seedance-2-0 |
video | content, ratio, duration, resolution, output_format, generate_audio, watermark, seed, bitrate_mode, return_last_frame |
spicy/seedance-2-0-mini |
video | content, ratio, duration, resolution, output_format, generate_audio, watermark, seed, bitrate_mode, return_last_frame |
spicy/seedance-2-5 |
video | content, ratio, duration, resolution, output_format, generate_audio, watermark, seed, bitrate_mode, return_last_frame, omni_reference_task_type, draft |
spicy/seedream-5-pro |
image | prompt, image, size, output_format, response_format, watermark, seed, layer_decomposition, background |
spicy/wan2.2-i2v |
video | image, last_image, prompt, duration, resolution, prompt_extend, seed |
spicy/wan2.7-i2v |
video | image, prompt, negative_prompt, audio_url, audio, resolution, duration, prompt_extend, seed |
spicy/z-image |
image | prompt, width, height, seed, prompt_extend |
xai/grok-imagine |
image | prompt, nsfw_checker |
xai/grok-imagine-edit |
image | prompt, image_urls, nsfw_checker |
52 models. Generated from each model's supported_openai_params — the same list /model/info serves, so this table and the gateway cannot disagree.
Two fields are accepted everywhere on /v1 and are the gateway's own: model, and user (your
end-user id, recorded against the job's spend and stripped before the request reaches the generator).
On /v2 neither is one of these: model is a top-level envelope field rather than something you put
in input, and user is not accepted at all — the nine fields listed for sociaro/minimax-h3-mini
are the whole of what its input takes, and anything else is 400 unknown field(s).
The shape every /v1 route shares
Everything from here on is /v1, except the rows that say /v2. /v2/jobs — the only way to reach
sociaro/minimax-h3-mini — answers with id rather than job_id, has six statuses
(queued, running, succeeded, failed, canceled, expired) rather than the three below,
returns outputs[].url rather than media_url, and its links do not expire in an hour. Read
Jobs API for it; do not generalise the examples below to it.
# 1. submit
curl -sS https://api.sociaro.com/v1/spicy/generations \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "model": "spicy/z-image", "prompt": "neon-lit rooftop at night, rain" }'
# -> { "job_id": "<id>", "status": "submitted" }
# 2. poll every 3-5 s for images, 5-10 s for video
curl -sS https://api.sociaro.com/v1/spicy/generations/<id> -H "Authorization: Bearer $KEY"
# -> { "job_id": "...", "status": "processing" }
# -> { "job_id": "...", "status": "completed", "model": "...", "media_url": "https://api.sociaro.com/v1/media/<media_id>" }
# -> { "job_id": "...", "status": "failed", "model": "...", "error": { "code": "...", "message": "...", "provider_code": "..." } }
# 3. download inside the hour — the opaque id IS the credential, no Authorization header
curl -sSL "https://api.sociaro.com/v1/media/<media_id>" -o out.mp4
Gate on the JSON status, never on the poll's HTTP code. A rejected or moderated generation comes
back as HTTP 200 with status: "failed". On /v1, processing and completed/failed are the only states;
completed always carries media_url, failed always carries error with both code and message.
The error object below is /v1/spicy/* and /v1/alibaba/* only. The other two doors do not share
it: /v1/byteplus/* hands back the generator's own envelope untouched, so its error.code is the
vendor's rather than one of ours and there is no provider_code; and the webhook /v1/images/async
posts on a failure carries job_id, status and model with no error object at all — treat
status: "failed" there as the whole message.
Match on error.code, read error.message. code is ours and comes from a small fixed set
(provider_failed, invalid_request, …). message is the generator's own sentence and
provider_code its own code, present when it gave one — that is what separates a moderation refusal
from a renderer fault. The names of the services and hosts that actually run the job are substituted
out of that text before it is sent; the halves of the model id you called (alibaba, bytedance,
wan, qwen, seedance, seedream, happyhorse) are deliberately left alone, or a message like
Model alibaba/wan-2-7-image not found would come back garbled. The text can quote your own request
back, so show it rather than store it.
message is variable text, up to 400 characters — do not match on it. It used to be one fixed
43-character sentence, so anything comparing it by equality or storing it in a narrower column needs
changing. code is the stable thing to branch on, and provider_code the finer-grained one; both are
short tokens.
The submit response's job_id is the value you put in the poll URL. Some routes name it id instead
(see the per-route notes) — it is the same value either way.
Where the routes differ
| Route | Body shape | Result field | Notes |
|---|---|---|---|
/v1/spicy/generations |
the model's own fields | media_url |
Nine models, three brokers behind one surface — you never learn which. Full field tables: each model's own page |
/v1/alibaba/generations |
prompt + resolution/duration/ratio… |
media_url; image models also media_urls |
HappyHorse and Wan video, and the Wan 2.7 / Qwen Image 3.0 image models, served direct from our own workspace. An image job that makes several pictures (n above 1) lists every one in media_urls (the generator's order, one link and one expiry each, media_url is the first) and is charged for the number of images the generator reports making (never more than you were given links to; if it lists extra links you get them all and pay for its count); if one cannot be delivered the job is failed and uncharged |
/v1/byteplus/contents/generations/tasks |
BytePlus's own content[] array |
content.video_url and media_url |
Pass-through: we forward the vendor's format verbatim. Submit returns id, not job_id |
/v1/byteplus/images/{async,generations} |
BytePlus's own image fields | data[].url and media_url |
Pass-through, native vendor model ids. async polls; generations answers in one call |
/v1/images/async |
model, prompt, webhook, … |
delivered TO your webhook | The one route with no poll — you give us a public https webhook and we POST the result to it |
/v2/jobs |
{model, input, output?, callback_url?, prompt_enhance?} — the model's own fields go inside input, and the envelope is closed (a key it does not name is a 400, user included) |
outputs[].url |
The one route that is NOT /v1, and the only way to reach sociaro/minimax-h3-mini. Different envelope, different status codes, Idempotency-Key required on submit, and a link that does not expire in an hour — a stable cdn.sociaro.com address on a CDN deployment, else an authenticated /v1/media/… one, so read the host you were given. Everything else — the full field reference and the per-model input schema from GET /v2/models — is in Jobs API, not this page |
On the two BytePlus pass-through routes the vendor's own raw url (content.video_url, data[].url) is
still returned for compatibility, and media_url was added beside it. Prefer media_url: it points at
our host, hides the vendor's storage, and expires. The raw field will be removed once nobody reads it.
A Seedance video job submitted with return_last_frame gets a second asset — the closing still. On
/v1/spicy/generations it arrives as last_frame_url; on the BytePlus pass-through as
content.last_frame_url (the vendor's own raw link) with last_frame_media_url beside it, on the
same terms as the clip. It costs nothing extra.
/v2 jobs do not return this still yet: return_last_frame is accepted there, but the closing frame is not
delivered and the result carries the clip alone. Use /v1 when you need it.
Treat the masked field as optional rather than guaranteed. It appears when you asked for it, the
generator returned a still, and that link passes the same delivery check the clip passed — if it
does not, we omit the field rather than hand you a link /v1/media would refuse. The clip is
unaffected either way: a still we cannot serve never costs you the video you paid for.
Layer decomposition — one picture in, up to 17 out
Seedream 5.0 Pro can take a finished image apart. Send layer_decomposition: true on
POST /v1/byteplus/images/async with model: "dola-seedream-5-0-pro-260628" (or
bytedance/seedream-5-pro) and exactly one image, and you get back a base image plus up to 16
layers, each a PNG with an alpha channel. A prompt is optional here: without one the model decides
what the separable elements are; with one it decomposes what you describe.
Each entry in data[] then carries three extra fields:
| Field | What it is |
|---|---|
z_index |
stacking order — 0 is the base image, layers start at 1, higher sits on top |
name, description |
what the model thinks the layer is |
bounding_box |
where it sits, as absolute [left, top, right, bottom] in base-image pixels, and normalized on a 0–1000 grid |
To rebuild the picture, place each layer at (left, top), scale it to right-left × bottom-top, and
stack in ascending z_index.
Price. Decomposition is billed per output image at half the generation rate — $0.0225 up to
2.61 Mpx, $0.045 above — so a 2K decomposition returning a base plus 16 layers costs about $0.84, not
one flat fee. Layers in the same response can land in different pixel tiers and are each charged on
their own. size here takes 1K, 1.5K, 2K or auto (the default), which sizes each output
from its own dimensions in your input.
Two limits worth knowing before you send one: exactly one input image (more is an error), and no partial success — if any single layer fails, the whole request fails.
This is a pass-through-only feature. It is refused on POST /v1/spicy/generations and that is
deliberate, not an oversight: the spicy poll answers with a single media_url, so a decomposition
there would charge you for seventeen images and hand you one. The refusal is free.
background — a transparent result
Also Seedream 5.0 Pro, also pass-through: background: "transparent" returns an image with a live
alpha channel instead of a filled one. It works only on image-to-image with exactly one input
image that itself has an alpha channel, and the output is PNG — asking for output_format: "jpeg"
alongside it is an error, as is feeding it a JPEG. Those refusals come from the generator in its own
words.
Delivery: media_url is a capability, not a bearer URL
It points at https://api.sociaro.com/v1/media/{id} and needs no Authorization header — the opaque
id itself is the credential, so anyone holding the link can fetch the asset until it expires. Treat it
like a signed URL: do not log it, do not forward it, and copy the asset into your own storage within
the hour.
The generator's own storage host is never exposed through media_url.
Status codes
| HTTP | Meaning |
|---|---|
200 |
submitted · processing · completed — or a failed job; check the JSON status |
400 |
bad body: unknown, malformed or out-of-range field, an unsupported model, or the generator's own validation error passed through on submit. On /v1/spicy/* that body is the generator's own, forwarded whole — its shape, its field names and its sentences — with only the names of the services and hosts substituted out, the same substitution a failed poll's error.message gets; parse it defensively, because those keys are the generator's. A refusal that is OURS — an unknown field, a resolution the model has no published rate for, or a 503 while we are briefly at capacity — arrives in our own {"error": {"message": …}} envelope instead, unaltered. Also something you SENT that we could not use: on /v1/spicy/generations/refs a reference THE GENERATOR REFUSED TO DOWNLOAD — a picture or a CLIP, since that route stages both — which names which one AND of which kind (reference image 2, reference video 2) and reports the fetch status — that one class, and only it, is remapped to 400, because the link is yours; terminal, nothing submitted and nothing charged, and a 4xx precisely so a client does not retry it automatically, though the same link may work once the source recovers. The other ways the reference stage can fail are NOT 400 and NOT terminal in the same way: the preparation service being unavailable, our own capacity, or the stage running out of its budget answer 503 — safe to retry, because nothing was submitted and nothing was charged, and those messages name NO kind, because they are about our preparation queue rather than about any one thing you sent; an asset call whose outcome we could not read answers 502 — do NOT retry that one blindly, since the asset may exist and a blind retry spends another of the three creates a minute we get account-wide |
401 |
missing or invalid key |
403 |
the key is not granted this model, or the job belongs to another key. A 401/403/407/429 that came from the GENERATOR (our account with it, not your key) is passed on with its status but with our own {"error": {"message": …}} body on /v1/spicy/* — that wording would be about our arrangement, not your request |
404 |
unknown or expired job id / media id |
413 |
body over the route's cap — send images as URLs, not inline base64 |
429 |
budget exceeded OR rate-limited — read error.message; they are different problems. That reading applies to OUR refusals. A 429 forwarded from the GENERATOR on /v1/spicy/* carries the neutral message instead (its wording is about our account with the vendor), so it cannot be told apart by message — treat it as transient and retry |
502 |
the generator gave us no usable job — retryable on poll, NOT on submit. (upstream_status=0 on a RelayError means the RELAY could not reach the vendor at all; that path carries no vendor code.) |
503 |
temporarily unavailable — retryable on poll; on submit only when it is a confirmed refusal |
429 — check which one it is
budget exceeded for this organisation keeps answering 429 until the organisation is topped up;
a rate limit clears on its own after a backoff. It is the ORGANISATION's balance in both the message
and the fix — keys carry no separate monetary ceiling, so there is no key setting to raise. Both are always safe to retry on poll. (Before 2026-08-05 an
over-budget key got a 502 here, which read as a gateway fault.)
An organisation that runs out of balance mid-generation can still poll and collect the job it already submitted — paid work in flight is never stranded. Submit is refused in that state.
502 on submit is NOT safely retryable
There is no idempotency key on these routes. A 502 can mean the generator never saw the request — or
that it accepted and started charging a job whose id never reached us. A network timeout after the
request was sent looks identical. Retrying creates a second paid generation. On a submit 502: stop,
and check whether a job appeared before sending anything again. The same caveat applies to a submit 503.
400, 401, 403, 404 are terminal, and so is a failed job — resubmit only if you want a fresh,
separately billed attempt.
Input URLs
Every generator fetches your input images and videos itself. The gateway never downloads or rewrites them. So a URL must be:
- publicly reachable — a host only visible from your own network fails inside the generator;
- still valid at fetch time — a signed link that expires in seconds will not survive the queue;
- served to automated clients. Some hosts refuse them: Wikimedia answers
403to the fetcher, and the failure surfaces as animage/video_urlparameter error that says nothing about the host.
Inline base64 works only for small inputs and is capped per route (413 above). URLs have no cap of ours.
Billing
What is left, in one call: GET /v1/balance answers {"remaining": 312.58} — the same figure
the console shows: your ORGANISATION's limit minus what it has spent. That is the only balance there
is; keys and teams carry no separate budget, so every key in your organisation reads the same
number. When there is nothing left it answers 429 with no figures at all, which is deliberate: a
number you cannot spend is not an answer. It excludes work still running, because a generation is
billed on completion.
Charged against your organisation's balance when the generation completes — on the first completed poll for the submit-and-poll routes, and within the one call itself on the synchronous ones such as /v1/byteplus/images/generations, which has no poll. A failed job is not billed. What the
charge is computed FROM differs by model — per image, per second of output, or per vendor-metered token —
so read the per-model notes rather than assuming. Each model's own page has the exact rule for
the nine spicy models, including the THREE Seedance rows — spicy/seedance-2-0, -2-0-mini and
spicy/seedance-2-5 — which are metered from what was actually rendered rather than from what you
requested. On those three, omitting resolution bills the generator's own default tier, 720p — a
per-model default, not a gateway-wide one: spicy/wan2.2-i2v defaults to 480p and spicy/wan2.7-i2v
to 1080p, so check the model's own row rather than carrying this number across.
Still on those three: a tier the model does not have — 1080p or 4k on spicy/seedance-2-0-mini,
4k on spicy/seedance-2-5 — is refused with a 400 naming the ones it does. Only
spicy/seedance-2-0 offers all four. The check applies however you ask, including the legacy
--resolution / --rs prompt flag (--rs:4k and --rs=4k included),
because the vendor would otherwise accept the value, ignore it, and bill you for a 720p render.
A prompt longer than 100,000 characters (all text parts together) is refused with a 400. The vendor recommends far less
(about 1,000 words) but accepts more; the limit only keeps the check itself cheap.
(bytedance/seedance-2.0-fast carries the same 480p/720p limit, but it lives on the BytePlus route,
not /v1/spicy.)