provider/model ids as chat completions, draws on
the same balance, and lands in Activity and on your statement like any other
request. There are four doors: create, read, content and delete. Listing jobs
is not served and keeps its 404, and neither is remix.
Point your existing OpenAI client at https://api.hopscotchlabs.ai and give it a
Hopscotch API key. Nothing else about your code changes.
Base URL
How to
Create a video
POST /v1/videos starts one job and answers the provider’s job object for it,
with external URLs omitted. The body may be JSON or multipart form data. Send
JSON for text to video. Send multipart when the create carries an
input_reference image, which rides in the form as a file part under that
name. The body is capped at 8 MiB either way; a larger body is refused 413
before any hold is taken.
curl
modelis required. It takes the same three forms as any other door: a provider-pinned id, a routing-profile slug, or the workspace default. A bare id is refused 400 with the codemodel_id_not_pinned.GET /v1/modelslists the exact strings this key can call, and a model is offered only while it would serve on call.promptis required for text to video. A model that renders from an image permits an omitted prompt wheninput_referenceis uploaded. Each model states its own length limit; the table below lists the stated ones.secondsis whole seconds of video, 1 or more, as decimal digits such as"8"; JSON also accepts a positive safe integer. Absent, it is 4. An explicit null is refused. Each model accepts only the durations it publishes: a create naming anything else is refused 400 before anything is held.sizeis required, asWIDTHxHEIGHTin pixels, such as"1280x720". It must have a per-second rate on the model; a size without one is refused before anything is held, and so is a size the model does not publish. That refusal answers 503 with the codemodel_not_priceable: an unpublished size, such as1366x768on a Seedance model, is not a fault that clears, so do not retry it. Send a size the model publishes.input_referenceis a reference image, multipart only. A model that renders from an image only requires it; a model that renders from text only refuses it. Each model accepts only the image forms and sizes it publishes, listed below.
Durations, sizes and sound per model
Match the model half of your pinned id against the first column.GET /v1/models lists the exact strings this key can call, and that list is
authoritative where this table and the catalog disagree. Unknown means the
serving code states no rule; the provider’s own refusal arrives relayed. A
row marked not currently served names a model no live id serves today, so
check the catalog before calling one.
Where a model requires a duration, omitting
seconds does not fall back to
4: the create is refused 400 and nothing is held.
First frame and last frame
There is no last-frame field: the request carries five fields and none names a last frame, so no served model takes one. The first frame isinput_reference: on a model that accepts it, the uploaded image becomes the
first frame of the render. Whether a model requires, permits or refuses the
image is in the table above.
The life of a job
A create takes a hold ofseconds (4 when absent) times the model’s
per-second rate for the requested size, before anything is sent to the
provider. On a workspace-owned credential the hold is zero. On a Seedance
model the hold covers one second more than requested, because the render is
billed a frame past the seconds; what the job does not use is returned when
it settles. The hold stays open until the job ends.
Poll the job with GET /v1/videos/{id}:
curl
id, status
(queued, in_progress, completed, failed or cancelled), progress,
and the seconds and size the job carries. Its model field is rewritten
to the public id the create returned, and external URLs are omitted. A
terminal status seen on a poll settles the job’s money once; the read itself
writes no usage row and is charged nothing. One usage row is written per job,
when it settles, timestamped then.
What the job settles at:
- Completed: the seconds the provider reports, never above the hold. A job reporting zero seconds releases the hold and records served usage at zero charge; absent or unreadable seconds charge the full hold.
- Failed, or cancelled with the provider’s confirmation: nothing.
- Still open 3 hours after create: expired, charged its full hold. You do not need to poll for the job to settle, but poll to learn the outcome.
video_not_found, and the provider is not called.
When the provider reports the job completed, download the bytes:
curl
video/mp4, chunked, with no Content-Length and no Content-Disposition.
Asking for content before the job completes answers 409 with
the code video_not_completed.
To cancel, delete the job:
curl
Wan 2.2 models are the exception to the follow-ups above: content and
delete answer 400 there, because the provider serves neither through this
door. The source specification says a finished Wan job’s poll answer
supplies the expiring video_url instead, but the control plane strips
every external URL from every job object before answering, a behavior the
video-doors suite pins, so the handler wins and the specification sentence
is stale. The effect is that the bytes of a finished Wan 2.2 job cannot be
fetched through this API today; its poll still reports the job. A
deepinfra/p-video job is a narrower exception on the delete alone: the
provider cannot cancel, so its delete answers 405 with the type
invalid_request_error, and the job keeps running and settles normally.
Refusals
A create for a model the catalog does not list answers 404model_not_found, like any other door. A follow-up for a job this
account did not create answers 404 video_not_found. Content for a job that
is not completed answers 409 video_not_completed. A provider that cannot
be reached answers 502 provider_unavailable, and an answer that cannot be
read answers 502 invalid_provider_response; neither charges anything. See
Errors for the envelope every one of these arrives
in.
Next
Streaming
Read a chat reply as it is generated.
Credits and billing
How a hold becomes a charge.
Every response carries the same id on two headers:
x-hopscotch-request-id,
which is ours, and x-request-id, which is the one the official OpenAI SDKs
surface on their error objects. Every error envelope repeats it as
request_id. Quote it when you ask us about a request. Without it we are
guessing at which of your requests you mean.