spicy/seedream-5-pro
Seedream 5.0 Pro — BytePlus's top image model.
Text-to-image and image-to-image, with two things the rest of the family does not have: it can take a finished picture APART into editable layers, and it can return a live alpha channel.
Sizes are a parameter here, not separate model ids.
Renders in roughly two minutes, so it is served asynchronously — submit, then poll.
| Slug | spicy/seedream-5-pro |
| Kind | image |
| Vendor | bytedance |
| Endpoint | POST /v1/spicy/generations |
Charged per job from what the generator reports it rendered — your Logs show the exact figure for each one; see balance and billing.
Calling it
curl https://api.sociaro.com/v1/spicy/generations \
-H "Authorization: Bearer $SOCIARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "spicy/seedream-5-pro",
"prompt": "a paper boat on a rain-soaked street"
}'
# then poll the job id it returns:
curl https://api.sociaro.com/v1/spicy/generations/JOB_ID \
-H "Authorization: Bearer $SOCIARO_API_KEY"Parameters
Seedream 5.0 Pro — text-to-image, image-to-image, and layer decomposition
| Field | Type | Required | Default | Values | What it does |
|---|---|---|---|---|---|
prompt | string | yes* | — | text | What to generate. Optional in layer-decomposition mode, where the model finds the elements itself; required otherwise. |
image | string[] | no | — | public https urls | Present -> image-to-image. Exactly ONE url in layer-decomposition mode; more is an error. |
size | string | no | 2K (auto when decomposing) | 1K · 1.5K · 2K, or WxH in pixels | 1.5K costs the same as 1K and looks better. Pixels are the only way to ask for a shape other than a square: area up to 4,624,220 px, aspect within 1:16..16:1. In decomposition mode only the presets and `auto` are accepted. |
layer_decomposition | boolean | no | false | true · false | Takes one image apart into a base image plus up to 16 PNG layers with alpha. The poll then returns `layers[]` beside `media_url`. Charged per output image at half the generation rate — seventeen charges, not one. |
background | string | no | opaque | transparent · opaque | `transparent` keeps a live alpha channel. Image-to-image only, with exactly one input image that already has alpha; output is png, so `output_format: jpeg` beside it is an error. |
output_format | string | no | jpeg | jpeg · png | In decomposition mode this governs the base image only — layers are always png. |
response_format | string | no | url | url | The async route is url-only; `b64_json` is refused, because buffering full images would sink the relay. |
watermark | boolean | no | false here | true · false | BytePlus defaults this ON; we default it off. Set true if you want the mark. |
seed | integer | no | random | — | Accepted and forwarded. NOT in the vendor's published field list, so treat reproducibility as unverified. |
Anything not listed here is refused rather than ignored, so a typo fails loudly instead of quietly producing something else.
Worked example
curl https://api.sociaro.com/v1/spicy/generations \
-H "Authorization: Bearer $SOCIARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "spicy/seedream-5-pro",
"prompt": "a red paper boat on wet asphalt at night, neon reflections",
"size": "1.5K"
}'
# take a finished picture apart — base image plus up to 16 layers
curl https://api.sociaro.com/v1/spicy/generations \
-H "Authorization: Bearer $SOCIARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "spicy/seedream-5-pro",
"image": ["https://your-cdn.example.com/scene.png"],
"layer_decomposition": true
}'Taking a picture apart
With layer_decomposition: true the poll returns the base image in media_url and the layers
beside it, bottom to top:
{ "job_id": "...", "status": "completed", "model": "spicy/seedream-5-pro",
"media_url": "https://api.sociaro.com/v1/media/<base>",
"layers": [
{ "url": "https://api.sociaro.com/v1/media/<id>", "z_index": 1,
"name": "paper boat", "description": "a folded paper boat, wet, listing to port",
"bounding_box": { "absolute": [225, 442, 796, 1414],
"normalized": [220, 432, 777, 1000] } }
] }
To rebuild the picture: place each layer at (left, top), scale it to right-left x
bottom-top, and stack in ascending z_index. The base is z_index 0 and covers the whole
canvas, which is why it carries no box; normalized is the same rectangle on a 0-1000 grid, for
compositing onto a canvas of your own size.
Every layer url is a /v1/media link on the same terms as any other asset: opaque, its own
expiry, no Authorization header. A layer we cannot serve is omitted rather than returned
broken — the base image is unaffected.
Charged per OUTPUT image, at half the generation rate — so a decomposition returning a base plus sixteen layers is seventeen charges at that reduced rate, not one flat fee. Layers in the same response can land in different pixel tiers and are each charged on their own.
Two limits: exactly one input image, and no partial success — if a single layer fails, the whole request fails and nothing is charged.
Getting a transparent result
background: "transparent" returns a live alpha channel. It works only on image-to-image with
exactly one input image that ALREADY has alpha, and the output is png:
{ "model": "spicy/seedream-5-pro", "prompt": "drop the background",
"image": ["https://your-cdn.example.com/cutout.png"], "background": "transparent" }
Asking for output_format: "jpeg" beside it is an error, as is feeding it a jpeg. Both refusals
come from the generator in its own words.
Shapes other than a square
This model has no aspect-ratio field — pixels are the only way to ask for one. 2816x1584 gives
16:9, 1584x2816 gives 9:16. Both the area (up to 4,624,220 px) and the ratio (within 1:16 to
16:1) must hold, and the generator names whichever one you missed.
How a job runs
Generation takes longer than a request should wait, so it goes in three moves: submit, then poll, then collect.
# 1. submit — the model id plus the fields above
curl -sS https://api.sociaro.com/v1/spicy/generations \
-H "Authorization: Bearer $SOCIARO_API_KEY" -H "Content-Type: application/json" \
-d '{ "model": "…", "prompt": "…" }'
# -> { "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 $SOCIARO_API_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": … } }
# 3. collect within the hour — the opaque id IS the credential, no header needed
curl -sSL "https://api.sociaro.com/v1/media/<media_id>" -o out.mp4
Use the job_id verbatim as the path segment when polling: the two names are the same value.
Which submit and poll URL this model uses is at the top of this page, under Calling it.
Reading the poll
status is one of processing · completed · failed. Only completed carries media_url,
only failed carries error.
Gate on status, never on the poll's HTTP code. A poll-time failure is HTTP 200 with
status: "failed" and an error object of { "code", "message" }, plus provider_code when the
generator supplied one — on every failure branch, including one where the result could not be
delivered. A submit-time rejection is different: it comes back as its own HTTP status with the
reason in the body.
code is ours and stable; message is the generator's. Match on code: it comes from a small
fixed set, and provider_failed still means what it always meant. A rejection of what you sent —
a size out of range, an unusable input — additionally sets code: "invalid_request", because that is
a different thing for your code to do. message now carries the generator's own sentence, and
provider_code its own code (OutputVideoSensitiveContentDetected.PolicyViolation,
IPInfringementSuspect), so a moderated generation, a copyright refusal and a broken renderer are
finally distinguishable. Treat provider_code as informational: the vendors change these strings
without telling us, so branch on ours.
message is variable text, capped at 400 characters. It used to be a single fixed
43-character sentence, so if you compare it by equality or keep it in a narrower column, that needs
changing — match on code, and on provider_code when you need the finer distinction.
Two things that text is not. It does not name the generator behind the model: the names of the
services and hosts that actually run your job are substituted out before you see it, so do not parse
it for one. What it does not hide is a name you already have — the halves of the model id you
called (alibaba, bytedance, wan, qwen, seedance, seedream, happyhorse) stay as written,
because blanking those would garble the message exactly where it is useful: Model
alibaba/wan-2-7-image not found has to survive intact. And the text can quote your own request
back: a moderation message often contains the fragment it objected to. That is why we do not write
it into our own logs, and why forwarding or storing a failed poll's message is your decision to
make rather than something to do by default.
Collecting the result
media_url points at our host, not the generator's. It is a capability: the opaque id is the
credential, so no Authorization header is needed and anyone holding the link can fetch it. It
expires within the hour — download rather than store the link.
A job that asked for a second asset gets one beside the first on the same terms: Seedance's
return_last_frame arrives as last_frame_url, a decomposition's layers as layers[]. Treat
them as optional: they appear when you asked, the generator returned one, and the link passes the
same delivery check the main asset passed. If it does not, we omit the field rather than hand you
a link /v1/media would refuse — and the main asset is unaffected.
/v2 jobs do not return the Seedance still yet: return_last_frame is accepted there, but the
closing frame is not delivered, so a /v2 job's result carries the clip alone. Use /v1 when you
need the still.
Several images are not optional extras. An Alibaba image model asked for more than one picture
(n above 1) is charged for the number of pictures the generator reports making, and every link it
returns is delivered, never fewer than that number: media_urls lists
all of them in the generator's order (present on every image result, even a single one) and
media_url is the first. Each has its own link and its own expiry. If any one of them cannot be
delivered the job is reported failed and nothing is charged — unlike a still or a layer, an
image you paid for is never left out.
Rules the gateway adds
Two fields are ours and never reach the generator:
| Field | Notes |
|---|---|
model |
which model to run |
user |
your own end-user id. Recorded against this job's spend so you can attribute cost per end user, and stripped before the request leaves us, so the generator never sees it. Validated BEFORE the job is created, so a rejection is free: a string of at most 128 characters, no control characters, valid UTF-8. Omitting it, or sending null, is fine and simply records no end user |
An unknown field is always a 400. Most generators reject one themselves; some accept it,
ignore it, render something other than what you asked for and bill you — so we refuse it for you.
Either way you get 400 unknown field(s): [...] naming the offender, never a surprise render.
Ranges and enums are the generator's own and we do not re-validate most of them, so an
out-of-range value comes back as its rejection, in its wording and with its numbers. The one thing
we DO check ourselves is resolution, because an unsupported tier is silently ignored rather than
refused: you would ask for the higher tier, be handed a lower one, and be charged. The tiers on
this page are read from what the vendor prices, so they change when the vendor publishes one.
Authentication
Authorization: Bearer <your-api-key> on submit and on poll. The key must be valid AND granted
this model — 403 otherwise.
One deliberate exception on poll: a key that is over budget may still poll and collect a job it already submitted, so work you have already paid for is never stranded by a budget that ran out mid-generation. While the key is over budget the gateway cannot read its model list at all, so the per-model grant is not re-checked on those polls — a grant revoked after submit still collects that job. Ownership is always enforced: a job can only be polled by the key that created it, and submit is always refused in that state.