Video generations
Generate video from a text prompt, optionally guided by reference images, video or audio.
Video generation is asynchronous. You submit a job, receive an id immediately, then poll until the video is ready. It is billed on the real usage reported for the finished job, not a fixed per-video price — see Billing.
POST https://api.ecohash.com/v1/video/generations
Video models are listed by GET /platform/models (category video) —
/v1/models lists text models only. The machine-readable spec for every
endpoint here is in the OpenAPI reference.
Generation modes
A request is in exactly one of four modes, chosen by what you put in
reference_images. The modes cannot be combined — a request mixing them is
rejected with 400.
| Mode | You supply | Control you get |
|---|---|---|
| Text to video | prompt only | The model composes everything |
| First frame | prompt + 1 image, role: "first_frame" | The video opens on your image |
| First and last frames | prompt + 2 images, first_frame and last_frame | Both ends pinned; the model fills the middle |
| Multimodal reference | prompt + 1–9 images, role: "reference_image" (± video, ± audio) | Images guide style and subject; no frame is pinned |
The distinction between the last two matters. Reference images influence the whole clip but do not pin any frame. If you need the video to begin or end on a specific image, use the frame roles — describing it in the prompt is not equivalent.
Quick start
curl https://api.ecohash.com/v1/video/generations \
-H "Authorization: Bearer eco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ecolink-video-gen-2.0",
"prompt": "a red balloon drifting over a mountain range at dawn",
"resolution": "720p",
"duration": 5,
"aspect_ratio": "16:9"
}'
Parameters
Required
| Field | Type | Notes |
|---|---|---|
model | string | ecolink-video-gen-2.0 |
prompt | string | What to generate. Required in every mode, including when you supply reference media |
Prompt language — English, Japanese, Indonesian, Spanish and Portuguese. Keep it under about 1000 words. Longer prompts scatter the model's attention: it starts tracking the main subjects and dropping the details, so elements you asked for go missing.
Output controls
| Field | Type | Default | Valid values |
|---|---|---|---|
resolution | string | 720p | 480p, 720p, 1080p, 4k |
duration | integer | 5 | 4–15 (seconds) |
aspect_ratio | string | adaptive | 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive |
generate_audio | boolean | true | true, false |
resolution sets the output size and the pricing band, so it is the single
parameter with the largest effect on cost. 480p and 720p share a band;
1080p and 4k each have their own. 4k output is 10-bit H.265 — richer
colour, but some players will not open it.
duration is clamped, not rejected: below 4 becomes 4, above 15
becomes 15. You get a video rather than an error, so if the length surprises
you, check what you sent. Duration is a direct input to the bill.
aspect_ratio defaults to adaptive, which picks a ratio to suit the
request — from the prompt for text-only jobs, from the first reference image or
video when you supply one. The ratio actually used is returned on the finished
job. If your chosen ratio does not match a reference image, the image is cropped
from the centre.
generate_audio is on by default — the model composes speech, effects
and background to match the prompt and picture. Set false for silent video.
For spoken lines, put the words in double quotes in your prompt, e.g. the captain turns and says "we sail at dawn". Generated audio is mono regardless of
any reference audio you supply.
Reference media
| Field | Type | Notes |
|---|---|---|
reference_images | object[] | Up to 9. Each entry is { "url": "...", "role": "..." } |
reference_image_urls | string[] | Shorthand: a plain URL list, all treated as reference_image |
reference_video_urls | string[] | Up to 3 reference videos |
reference_audio_urls | string[] | Up to 3 reference audio tracks |
reference_video_url | string | A single reference video — shorthand for a one-element reference_video_urls |
reference_audio_url | string | A single reference audio track — shorthand for a one-element reference_audio_urls |
The singular and plural forms are mutually exclusive for a given kind —
send reference_video_url or reference_video_urls, not both. Likewise
reference_images and reference_image_urls. Sending both returns 400.
When you supply several videos or several audio clips, the combined duration of each kind must stay within 15 seconds — see Input media requirements.
Image roles
role | Meaning | Count |
|---|---|---|
first_frame | The video opens on this image | 1 |
last_frame | The video ends on this image. Requires a first_frame | 1 |
reference_image | Guides style and subject; pins nothing | 1–9 |
| (omitted) | Treated as reference_image | — |
Rules, all enforced before the job is created:
reference_imagecannot be mixed withfirst_frameorlast_frame.last_framewithout afirst_frameis rejected.- At most one
first_frameand onelast_frame. first_frame/last_framecannot be combined with reference video or audio.- Reference audio needs at least one image or a reference video — audio cannot be the only reference.
The first and last frame may be the same image. If the two have different aspect ratios, the first frame wins and the last is cropped to match.
Omitting role means reference_image, never first_frame. If you want the
opening frame pinned, say so explicitly.
Accepted but not used by this model
These belong to the shared video request shape and are accepted without error,
but ecolink-video-gen-2.0 does not act on them. Listed so you can tell a
silently-ignored field from one that is not working.
| Field | What happens |
|---|---|
seed | Ignored. This model has no seeded output; identical requests will differ |
num_frames | Only derives duration (num_frames ÷ fps) when duration is omitted. Never sets frame count |
fps | Only derives duration, as above. Does not set the output frame rate |
width, height | Ignored. Use resolution and aspect_ratio |
num_inference_steps | Ignored |
Watermarking is off for all generated video and cannot be enabled.
Input media requirements
Every reference field takes any of three forms — mix them freely in one request, including images, video and audio together:
| Form | Example | Use it for |
|---|---|---|
| URL | https://cdn.example.com/portrait.jpg | Media already hosted anywhere publicly downloadable |
Inline data: URI | data:image/png;base64,iVBORw0KG… | Sending the file in the same request — no hosting, no extra call |
| Uploaded file | the url returned by POST /v1/uploads/reference | Large files, or reusing one file across requests |
Inline media must be base64 (data:<type>;base64,<data>). The type is
detected from the file itself, and must match the field: images in the image
fields, video in the video fields, audio in the audio fields. Limits: 50 MB
per inline item after decoding, and 100 MB for the whole request body.
Above that, upload the file instead. A malformed or mismatched inline item
returns 400 naming the field (for example reference_images[0].url); an
oversized one returns 413 stating the limit.
Uploading suits files above the inline limits (up to 200 MB) or files you reuse:
curl https://api.ecohash.com/v1/uploads/reference \
-H "Authorization: Bearer eco_YOUR_KEY" \
-F "file=@portrait.jpg"
{ "id": "3f2c…e1.jpg", "url": "https://…", "expires_in": 1800, "content_type": "image/jpeg", "size": 248113 }
Pass url in the generation request. It is valid for 30 minutes; keep id
and call GET /v1/uploads/reference/{id}/url for a fresh one later.
The per-type rules below apply to every form.
These are checked when the job is submitted, so a file that breaks a rule comes
back immediately with a code of reference_media_unreachable or
reference_media_too_long.
Images
| Formats | JPEG, PNG, WebP, BMP, TIFF, GIF, HEIC, HEIF |
| Dimensions | 300–6000 px on each side |
| Aspect ratio | between 0.4 and 2.5 (width ÷ height) |
| Size | 30 MB per image |
Video
| Container | MP4 (.mp4), QuickTime (.mov) |
| Video codec | H.264/AVC, H.265/HEVC |
| Audio codec | AAC, MP3 |
| Resolution | 480p, 720p, 1080p, 4k |
| Duration | 2–15 seconds each; with several videos, 15 seconds combined |
| Count | up to 3 |
| Dimensions | 300–6000 px per side; total pixels between 409,600 and 8,295,044 |
| Aspect ratio | between 0.4 and 2.5 |
| Frame rate | 24–60 fps |
| Size | 200 MB each |
Audio
| Formats | WAV, MP3 |
| Duration | 2–15 seconds each; with several clips, 15 seconds combined |
| Count | up to 3 |
| Size | 15 MB each |
Output dimensions
resolution and aspect_ratio together determine the exact pixel size:
| Resolution | 16:9 | 4:3 | 1:1 | 3:4 | 9:16 | 21:9 |
|---|---|---|---|---|---|---|
| 480p | 864×496 | 752×560 | 640×640 | 560×752 | 496×864 | 992×432 |
| 720p | 1280×720 | 1112×834 | 960×960 | 834×1112 | 720×1280 | 1470×630 |
| 1080p | 1920×1080 | 1664×1248 | 1440×1440 | 1248×1664 | 1080×1920 | 2206×946 |
| 4k | 3840×2160 | 3326×2494 | 2880×2880 | 2494×3326 | 2160×3840 | 4398×1886 |
With aspect_ratio: "adaptive" the model picks a row; the finished job reports
which.
Examples
First frame
The video opens on your image and animates forward.
curl https://api.ecohash.com/v1/video/generations \
-H "Authorization: Bearer eco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ecolink-video-gen-2.0",
"prompt": "the boat pulls away from the dock as gulls scatter",
"resolution": "1080p",
"duration": 6,
"reference_images": [
{ "url": "https://example.com/harbour.jpg", "role": "first_frame" }
]
}'
First and last frames
Both ends are pinned; the model generates the transition between them.
curl https://api.ecohash.com/v1/video/generations \
-H "Authorization: Bearer eco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ecolink-video-gen-2.0",
"prompt": "slow push in from the wide harbour view to the lighthouse at dusk",
"resolution": "1080p",
"aspect_ratio": "16:9",
"duration": 8,
"reference_images": [
{ "url": "https://example.com/harbour-wide.jpg", "role": "first_frame" },
{ "url": "https://example.com/lighthouse.jpg", "role": "last_frame" }
]
}'
Multimodal reference
Several images guide subject and style; no frame is pinned. A reference video can supply motion, and reference audio can supply timing.
curl https://api.ecohash.com/v1/video/generations \
-H "Authorization: Bearer eco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ecolink-video-gen-2.0",
"prompt": "a documentary shot of the same harbour in winter, muted colour grade",
"resolution": "720p",
"duration": 10,
"reference_images": [
{ "url": "https://example.com/style-1.jpg", "role": "reference_image" },
{ "url": "https://example.com/style-2.jpg", "role": "reference_image" }
],
"reference_video_urls": ["https://example.com/camera-move.mp4"],
"reference_audio_urls": ["https://example.com/ambient-timing.mp3"]
}'
Each of reference_video_urls and reference_audio_urls takes up to 3. The
singular reference_video_url / reference_audio_url still work as a one-item
shorthand, but prefer the list form.
The shorthand form is equivalent when every image is reference material:
"reference_image_urls": ["https://example.com/style-1.jpg", "https://example.com/style-2.jpg"]
Silent, vertical, cheapest band
curl https://api.ecohash.com/v1/video/generations \
-H "Authorization: Bearer eco_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ecolink-video-gen-2.0",
"prompt": "rain running down a window, neon reflections",
"resolution": "480p",
"aspect_ratio": "9:16",
"duration": 4,
"generate_audio": false
}'
Response
202 Accepted — the job is queued. No video yet.
{
"id": "b3f1c2a4-...",
"status": "processing",
"model": "ecolink-video-gen-2.0"
}
Poll for completion
GET https://api.ecohash.com/v1/video/jobs/{id}
status moves queued → processing → completed, or ends at failed.
Generation takes minutes; poll every few seconds rather than continuously.
{
"id": "b3f1c2a4-...",
"status": "completed",
"model": "ecolink-video-gen-2.0",
"width": 1280,
"height": 720,
"created_at": "2026-07-14T09:00:00Z",
"completed_at": "2026-07-14T09:01:12Z",
"url": "https://.../b3f1c2a4.mp4",
"billing": {
"charge_usd": 0.42,
"pricing_tier": "480_720_no_video",
"resolution": "720p",
"duration_seconds": 5,
"billable_tokens": 100000,
"usage_source": "upstream_reported"
}
}
url is a temporary download link valid for 30 minutes. Fetch and store the
file if you need it longer; polling again issues a fresh link.
On failure, status is "failed" and the job carries three fields describing
what went wrong:
{
"id": "b3f1c2a4-...",
"status": "failed",
"model": "ecolink-video-gen-2.0",
"created_at": "2026-07-14T09:00:00Z",
"completed_at": "2026-07-14T09:00:48Z",
"error": "The generated audio was flagged by the content-safety filter — please try a different prompt, or turn off audio generation.",
"code": "content_filtered.output_audio",
"retryable": false
}
| Field | Use it for |
|---|---|
code | Branching in your client — a stable value, listed under Errors |
retryable | Whether resubmitting the same request could succeed |
error | Showing a person what happened. Wording changes; do not match on it |
Failed jobs are not billed.
Estimate cost first
POST https://api.ecohash.com/v1/video/quote
Takes the same body as a submit and returns the pricing band and an estimated range without generating anything.
{
"pricing_tier": "480_720_no_video",
"unit_price_per_1m_tokens": 7.14,
"estimated_tokens_min": 85000,
"estimated_tokens_max": 115000,
"estimated_cost_min_usd": 0.607,
"estimated_cost_max_usd": 0.821,
"currency": "USD",
"note": "Final charge is based on actual tokens reported by the provider."
}
Billing
A finished job is charged on the tokens reported for it, at a rate set by two things:
| Resolution band | 480p/720p · 1080p · 4k |
| Reference video | whether reference_video_url was supplied |
Six rates in total. Supplying a reference video uses a lower per-token rate than generating from a prompt alone.
Because the charge follows real usage, a quote is a range rather than a fixed
price. duration and resolution move it most.
For current rates call GET /platform/models/ecolink-video-gen-2.0/pricing,
which returns the live per-band figures — read them rather than hard-coding.
Failed jobs are never charged.
Limits
- 10 submissions per 60 seconds per account for this model. Over it returns
429. - Failed submissions do not count, so a rejected request can be retried immediately.
- Up to 3 reference videos and 3 reference audio tracks per request, each kind capped at 15 seconds combined.
- Reference media must be a URL; base64 payloads are not accepted.
Errors
A failure — whether it comes back from the submit call or shows up later on a
polled job — carries a code. Branch on that. The error text is written for
people and is reworded whenever we can explain something better, so matching on
it will break.
{
"error": "A reference image was rejected by the content-safety filter — please replace it and try again. It may contain an identifiable real person.",
"code": "content_filtered.input_image"
}
Error codes
code | HTTP | Retry? | What to change |
|---|---|---|---|
invalid_request | 400 | no | A parameter is missing or out of range. Check resolution, aspect ratio and duration against Parameters. |
reference_media_unreachable | 400 | no | A reference URL could not be fetched, or the file was not readable media. It must be publicly downloadable. |
reference_media_too_long | 400 | no | A reference clip's duration is out of range — each audio or video must be 2–15 seconds, and several of one kind must total ≤15 seconds. |
content_filtered.input_text | 400 | no | Your prompt was rejected. Rephrase it. |
content_filtered.input_image | 400 | no | A reference image was rejected. Replace it. |
content_filtered.input_video | 400 | no | A reference video was rejected. Replace it. |
content_filtered.input_audio | 400 | no | A reference audio clip was rejected. Replace it. |
content_filtered.output_text | 400 | no | Generated text was flagged. Try a different prompt. |
content_filtered.output_image | 400 | no | A generated image was flagged. Try a different prompt. |
content_filtered.output_video | 400 | no | The generated video was flagged. Try a different prompt. |
content_filtered.output_audio | 400 | no | The generated audio was flagged. Try a different prompt, or set generate_audio to false. |
capacity_exceeded | 429 | yes | Generation capacity is saturated. Wait and retry. |
generation_timeout | 504 | yes | Generation ran past its deadline. Retry, or reduce duration / resolution. |
internal_error | 502 | yes | A temporary fault. Retry. |
model_unavailable | 502 / 503 | no | The model cannot serve requests right now. This one is ours, not yours — retrying the same request will not help. If it persists, contact support. |
The HTTP column applies when the error comes back from the submit call. A job
that fails during generation returns 200 on the poll with status: "failed" —
there the code is the only signal, which is why it is worth branching on.
Two shortcuts worth building in:
content_filtered.prefix — match the prefix to handle every content-safety rejection in one branch, and the exact code when you want to tell a user which file to swap. New modalities added later keep working with a prefix match.retryable— polled jobs also return a booleanretryable, so you can drive backoff from it without keeping a list of codes in your client.
Where errors surface
Your job is submitted for generation as part of your request, so problems with the request itself — a missing parameter, an unreachable reference URL, an over-long clip — come back immediately rather than as a failure discovered minutes later.
Content-safety rejections can happen at either point. A reference file is
checked on submit; generated output can only be checked once it exists, so
content_filtered.output_* always arrives on a polled job.
A job accepted with 202 can still end at status: "failed". Check the polled
job rather than assuming acceptance means success. Failed jobs are not billed.
Authentication and account errors are not video-specific: 401 for a missing or
invalid API key, 402 for insufficient credits, 404 for an unknown model. See
Errors for those.
Render on your own GPU instance
If you run a video model on your own GPU instance (for example the LTX 2.5 Video Generation template), you can send it jobs through this same API instead of calling the instance directly. A render can take minutes; as a job it keeps running even if your client disconnects, and you fetch the finished file afterwards.
Add instance_id to the request:
curl -X POST https://api.ecohash.com/v1/video/generations \
-H "Authorization: Bearer $ECOLINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"instance_id": 1114, "prompt": "a butterfly over a field of flowers", "width": 1920, "height": 1080, "duration": 10, "seed": 42}'
# → 202 {"id": "...", "status": "queued", "model": "instance-1114", "target_instance_id": 1114}
Then poll for completion and download the video exactly as for any other job.
- The instance must be yours, running, and launched with a service port. Your server must answer
POST /v1/video/generationswith{"data":[{"b64_mp4": "<base64 mp4>"}]}, as the LTX 2.5 template does. - Your parameters are passed to your server as given:
prompt,width,height,num_framesorduration(withfps),seed,num_inference_steps,guidance_scaleand reference images. Left out, the size defaults to 768×512,fpsto 24 and the length to 121 frames. Whether your server honours each one is up to the server: the LTX 2.5 template, for example, rounds sizes to multiples of 32 and ignoresnum_inference_stepsandguidance_scale. - The finished job reports what was produced:
resolution(for example1920x1088) andduration_seconds, next to what you asked for (width,height,num_frames,fps). It also carriestarget_instance_id. - One job renders at a time per instance, in order; up to 10 can wait. Beyond that the submit returns
429(instance_queue_full). - No charge per video. You already pay for the instance by the hour; the job's
billingreads{"status": "not_billable", "charge_usd": 0}. - A job is tried once. If your server fails or the instance stops, the job fails with the reason.
Related
- Video generation guide — worked examples
- Billing overview — how usage charges appear
- API keys