Jobs API — one envelope for every async model
/v2 is a single pair of routes for every model that renders asynchronously. Where /v1 gives each
vendor family its own submit path, its own poll path and its own response shape, here you always
POST /v2/jobs, always GET /v2/jobs/{id}, and always read the same fields back — whichever
model you called.
- Base URL:
https://api.sociaro.com - Auth:
Authorization: Bearer <your-api-key>, the same key you use on/v1. /v1is unchanged and stays supported. Nothing you already have needs migrating.
Submit
POST /v2/jobs, with an Idempotency-Key header — it is required. Replaying the same key returns
the same job; reusing it with a different body is a 409.
curl https://api.sociaro.com/v2/jobs \
-H "Authorization: Bearer $SOCIARO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-request-0001" \
-d '{
"model": "alibaba/wan-2-7-image",
"input": {"prompt": "a paper boat drifting down a rain-soaked street"}
}'
{"id": "job_5cc8778b79574aaa7ea8fc09caec23e5",
"model": "alibaba/wan-2-7-image",
"status": "queued",
"created_at": "2026-08-24T08:54:16.457268+00:00"}
The body has two required fields and two optional ones:
| field | what it is | |
|---|---|---|
model |
required | An id from GET /v2/models. |
input |
required | The model's own parameters, passed through unchanged. This is the one part of the envelope that differs per model — see below. |
output |
optional | count (how many outputs, up to the model's max_output_count) and format (url). count is the only way to ask for more than one — a vendor's own n inside input is refused, because two numbers for one question is how you get billed for one render and charged for four. The same goes for a truthy enable_sequential (Wan 2.7's image-set mode, up to 12 images), refused with the same 400 invalid_input; enable_sequential: false is fine. An image model also refuses a parameters object inside input (400 invalid_input, "input.parameters is not accepted on this model — send generator fields directly in input, and the number of outputs as output.count"): put the generator's fields straight in input, and the number of outputs in output.count. Those are refusals at submit. If a generator nonetheless returns more result URLs than output.count, nothing is staged and nothing is charged, and the job stays running until it ends expired. |
callback_url |
optional | An https URL we POST to once the job reaches a terminal status, so you need not poll. |
prompt_enhance |
optional | How much work goes into your prompt before generation: "off" (default) sends it as written, "basic" has a fast language model rewrite it into a fuller shot description, "full" first has your reference images described and your reference audio transcribed by models that read them and writes the prompt from those findings (reference videos are not analysed). Only on models that support it (today sociaro/minimax-h3-mini; 400 elsewhere). The rewritten text is not returned; the work is billed as its own line and included in the job's cost_usd (only what the provider itself prices is charged on — transcribing audio is not charged to you today). If the rewrite cannot be produced (or the prompt is longer than 4,000 characters), your original prompt is used and nothing is billed for it — only the render. In "full" your reference images and audio leave Sociaro: they are sent to a third-party model provider for analysis. |
What goes in input
input is handed to the generator as you wrote it. Which means the parameters are the ones documented
for that model, not a /v2 dialect — and the two families served here word their prompts differently:
Alibaba models (alibaba/*) take a plain prompt string:
{"model": "alibaba/wan-3-video", "input": {"prompt": "a paper boat drifting down a rain-soaked street"}}
ByteDance Seedance models (bytedance/seedance-*) take the vendor's content array, the same
shape as on /v1:
{"model": "bytedance/seedance-2.0",
"input": {"content": [{"type": "text", "text": "a paper boat drifting down a rain-soaked street"}],
"resolution": "480p", "duration": 4, "generate_audio": false}}
GET /v2/models carries an input_schema per model, and it is a real one: the required field, every
option that model takes and /v2 delivers (so return_last_frame is left out, see below), its
allowed values, and — where the vendor publishes one — its limit. The
resolutions and durations differ per model and the schema says which are which, so you can build a
request from it rather than from a table.
Two things it does not do. It is descriptive, not enforced: additionalProperties is true
because your input reaches the generator unchanged, so a field the schema does not name is not
refused here — the generator decides (the exceptions are the fields listed under output). And it does not repeat what each model's own page explains in
prose. Everything that works on /v1 works here, except one result: /v2 does not return the closing
still that return_last_frame asks for, so the option is left out of the schema and a job that sets it
results in the clip alone.
One /v1 limit behaves differently here. The text of all content parts together may be at most 100,000
characters (the vendor recommends about 1,000 words, so this only catches a runaway prompt). On /v1 a
longer prompt is a 400 at once; on /v2 the check runs when the job is dispatched, so the job is
accepted and then ends failed with provider_rejected, uncharged. A draft or a final (below) is checked
at submit and answered with a 400.
Draft mode (Seedance 2.5)
bytedance/seedance-2.5 can render a cheap 480p draft first, so you can judge the shot before paying
for the finished clip. It is a second job that names the first one. Send "draft": true in input:
{"model": "bytedance/seedance-2.5",
"input": {"content": [{"type": "text", "text": "a paper boat drifting down a rain-soaked street"}],
"duration": 4, "draft": true}}
The answer is an ordinary job, job_5cc8778b79574aaa7ea8fc09caec23e5. When it has succeeded, render the
1080p final by quoting that job_... id (ours, not the generator's) in a draft_task part, with a new
Idempotency-Key:
{"model": "bytedance/seedance-2.5",
"input": {"content": [{"type": "draft_task",
"draft_task": {"id": "job_5cc8778b79574aaa7ea8fc09caec23e5"}}]}}
- Send no
resolution./v2sets it on both steps,480pon the draft and1080pon the final. If you send one anyway it must be that step's own value, or the request is a400. (On/v1a final does have to carry"resolution": "1080p", because there your body goes to the generator as written. Here we build it, so you need not.) - The final reuses the draft's prompt, references, duration, ratio, seed and audio settings. You do not resend them.
- The two jobs are billed separately, each as an ordinary video at its own resolution. The final's token rate is the one the draft's inputs earn, so a final whose draft had an input video is charged the with-video rate, although the final's own body has no video.
- The draft has to be one of your own jobs, a draft, and
succeeded. A job of someone else's gets the same answer as an id that does not exist. Past the generator's own seven days a final built from it fails.
Poll
GET /v2/jobs/{id}. While the job is unfinished the response carries retry_after (seconds), which
mirrors the Retry-After header — wait that long rather than polling in a tight loop.
{"id": "job_5cc8778b79574aaa7ea8fc09caec23e5",
"status": "succeeded",
"model": "alibaba/wan-2-7-image",
"media": "image",
"created_at": "2026-08-24T08:54:16.457268+00:00",
"completed_at": "2026-08-24T08:55:28.624694+00:00",
"retention_until": "2026-09-23T08:54:16.457268+00:00",
"cost_usd": 0.036,
"outputs": [{"url": "https://cdn.sociaro.com/v2/job_5cc8778b79574aaa7ea8fc09caec23e5/0-3f21c8ead6af7a981b5bf99fd705a745.png",
"type": "image",
"content_type": "image/png",
"expires_at": "2036-08-24T08:55:28.608057+00:00"}]}
status is one of queued, running, succeeded, failed, canceled, expired. The last four are
terminal — stop polling.
The outputs[].url is a stable link on our CDN (https://cdn.sociaro.com/…). No key, no signature,
no deadline: fetch it with a plain GET, embed it, or store it — it keeps working after retention_until,
when the job record itself is gone. Results are kept for at least ten years (expires_at). The address is
unguessable and we only ever show it to the account that owns the job — but anyone who has the link can
fetch the file, so hand it out as you would the file itself.
If a url ever points at https://api.sociaro.com/v1/media/… instead (a deployment without the CDN
configured), that route needs an Authorization: Bearer header for any valid key of your account and
answers 404 without one. Read the host of the url you were given rather than assuming either form.
curl -L -o boat.png "https://cdn.sociaro.com/v2/job_5cc8778b79574aaa7ea8fc09caec23e5/0-3f21c8ead6af7a981b5bf99fd705a745.png"
Submitting the same request again with the same Idempotency-Key returns the finished job with its
outputs, so a lost response is never a lost result.
Cancel
POST /v2/jobs/{id}/cancel while a job is still cancellable. A job that already finished is returned
unchanged rather than treated as an error.
Failure
A failed job carries error, and no outputs:
{"id": "job_4be56281b681608b24640e8b82f56cf1",
"status": "failed",
"error": {"code": "provider_failed", "message": "the provider failed to complete this job"}}
A failed job is not charged. Its cost_usd stays absent and the amount held for it is released.
code comes from a fixed set, and these two are the ones worth telling apart:
provider_rejected— the model refused the request before rendering. Usuallyinput: a parameter it does not take, or the wrong prompt shape for that family (see above).provider_failed— the model accepted the request and then did not finish it. On Seedance this is most often the generated soundtrack rather than the picture: it composes audio by default and checks that audio after the video has rendered. Pass"generate_audio": falseif you do not need sound — it removes that whole class of failure and the clip renders the same.
The rest of the set, in the order you are likely to meet them: timeout (did not finish in time),
expired (exceeded its maximum runtime), canceled (you canceled it), quota_exceeded (your
account's storage is full), output_too_large (the generated file exceeded the allowed size),
unsupported_media (the output could not be stored) and internal (our fault). Match on code — a
value outside this list is not something the API produces.
/v2 tells you which code it was, and nothing more. These codes and their fixed wording are the
whole error envelope here; the generator's own sentence and code are not carried. The /v1 poll
for the same models does carry them (provider_code beside message) — see Images and video — so
if you need to tell a moderation refusal from a renderer fault while a job is running, that is where to
read it.
Submit-time errors use the shapes on the Errors page. Beyond those, 429 means either too many of
your jobs are in flight (honour Retry-After) or your organisation is out of balance.
Models
GET /v2/models is the list. There is no table here on purpose: a list printed on a page goes
stale the day a model is added, and this endpoint cannot. It answers with every model, its output
medium, its runtime ceiling and its input schema:
curl https://api.sociaro.com/v2/models -H "Authorization: Bearer $SOCIARO_API_KEY"
Video and image today — ByteDance's Seedance line and Alibaba's Wan, HappyHorse and Qwen models.
/v1 serves far more, including everything synchronous; Generations API lists which /v1 route
serves which model.
A model's entry carries its input_schema, and the required fields there are the whole minimal
request. Some models need a media input as well as a prompt — an image-to-video model needs its
first frame — and the schema names it.
Limits
- Video jobs get up to 30 minutes to render, images 5. Past a job's
deadline_atit is abandoned and reported asexpired. output.countcannot exceed the model'smax_output_count.- Billing is identical to
/v1— same rates, same account, one charge per successful job, visible in the same usage figures. See Billing.