Sociaro

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/videos serves no model in this document and answers 404 with error.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_generation on /model/info: that field says what a model produces, not where to send it. /v1/models returns only the OpenAI shape (id, object, created, owned_by), carries no routing information at all, and its owned_by is always the literal openai no 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 403 to the fetcher, and the failure surfaces as an image/video_url parameter 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.)