Skip to main content
Give Mesh a prompt and you get back a video. Give it more to work with — a still to open on, a second still to end on, reference images, a clip, a soundtrack — and you get back a video that matches what you supplied. Generating a video takes minutes rather than seconds, so this endpoint is asynchronous. Your POST returns a task ID straight away, and the finished video arrives either when you ask for it or through a webhook. Everything goes to https://api.meshapi.ai and carries your API key as Authorization: Bearer rsk_<your-key>. See Authentication if you need one.

Your first video

A prompt in, a task ID out.
The response is short, because there is no video yet:
Hold on to that id — Getting the result turns it into a video. Log unsupported_params beside it while you are there: this is the only response that ever carries it, and it tells you whether the request that ran was the request you sent. Three endpoints cover everything on this page:
The examples further down show the request body only. Send each of them exactly like the curl above — same URL, same two headers.

Giving the model something to work from

The content array carries your prompt and every piece of media that goes with it. Each item says two separate things:
  • type is the modality — text, image_url, video_url, audio_url or document_url.
  • role is what that media is for — first_frame, last_frame, reference_image, reference_video, reference_audio or reference_document (plus the finer roles below).
Keeping them apart is what lets one request carry two images that mean different things. Two image_url items are ambiguous on their own; a first_frame and a last_frame are not. Any of the URLs may be a publicly reachable link or a Base64 data URI (data:image/jpeg;base64,... — keep the MIME prefix). A public URL has to be fetchable without authentication, since the model fetches it itself. role is optional. Leave it out and each item falls back to its most common meaning: So the simplest image-to-video request needs no role at all. You need one as soon as a request pins a closing frame, or uses images as references rather than as frames. A role that contradicts its item — first_frame on an audio item — is rejected with 422. So is a role the model you picked does not offer: rather than drop the item and generate something you did not ask for, the submit refuses it and names the offending entry — by its position in content on most models, by role alone on the rest. Neither costs you a generation, but both cost you a round trip — check first.

Pick one input scenario, not two

Roles are not freely combinable. A request is one of these shapes, and on most models they are mutually exclusive:
Mixing two of them is the most common way a video request fails. A first_frame alongside a reference_image is not a richer request — it is an invalid one, and you often find out late: the submit returns 200 with a task ID and the task goes failed minutes later.
To pin both ends and supply references, use first-and-last-frame — it is the only shape that guarantees the output matches the images you gave. References can be nudged toward a particular opening with wording in the prompt, but nothing guarantees the match. Text to video is not universal either: a model published specifically for image-to-video or for upscaling needs its input media and turns down a prompt-only request with a 400.

Start from an image

One image and a prompt: the video opens on your still and moves from there. role is optional here, but writing it makes the intent obvious.

Open and close on exact frames

Give the model both ends and it fills in the middle. Here role is required — two images with no roles are ambiguous.
The two images may be identical, and they need not share an aspect ratio — the first frame wins and the last frame is cropped to match. A closing frame is a rarer ability than an opening one, and models that offer it usually want a first_frame with it.

Work from reference images

References say what should appear, not where in time it appears — a face, a product, a look. Use them when you want the same thing to show up in the video without dictating any particular frame.
How many reference images you may send ranges from one to thirty depending on the model. supports_video_reference_image tells you whether the role is accepted at all; the model governs how many.

Name your subjects

A flat list of five reference images is five unrelated references — nothing in it says that the first two are the same person and the third is the bottle she picks up. subject says so. Pick a name, put it on every item that shows that subject, and refer to it in the prompt as @name.
Four things to know:
Order matters inside a group: the first item is the main view, the rest are further angles of the same subject. Nothing about the request looks ordered, which is what makes this easy to get wrong — put the clearest, most head-on image first.
Names are strict and match exactly. 1–64 characters of letters, digits, underscore or hyphen. Spaces, @, dots and non-ASCII are rejected with a 422, as is an empty name, and matching is case-sensitive — Hero and hero are two different subjects. In the prompt, only names you actually declared are treated as citations; every other @ in your text is left exactly as you typed it. subject belongs on reference items only. On a first_frame, on a text item, or anywhere else it returns 422. Grouping never costs you anything. A model with no notion of subjects still receives every image as an ordinary reference, so the request above works wherever reference images do — it simply loses the distinction between the two subjects. You never need two versions of a request.
Not every model reads names. Some match reference images by the order you sent them instead, and there an @name is just text in your prompt — the images still reach the model, you only lose which-is-which. Describing your subjects in prose as well (“the man in the blue coat picks up the green bottle”) costs nothing and reads correctly either way.

Keeping a character consistent

Models that can hold one character across a whole video generally screen the reference first, for likeness and for copyright, and decline anything that looks like a real person. That screening is the usual reason a reference which “should” work appears to have been ignored. Where a model works that way, Mesh registers your reference before generating so the same face survives from shot to shot. There is nothing to switch on — send the image as a public URL or a data URI exactly as you would otherwise.
The first request with a brand-new reference can come back 422, saying the reference is still being prepared. Registration runs in the background with no fixed completion time. Retry the same request; the retry joins the registration already in flight rather than starting a second one, and after that the image is immediate.
References are matched on content, not on URL. The same image behind a re-signed link or a CDN variant is one reference, so changing the URL of an image you have already used costs nothing. Send the image, not an ID. asset:// identifiers from elsewhere are rejected with 422. Supply a URL or a data URI and Mesh does the registering.
By supplying a reference you confirm you hold the rights to it and that it is not the likeness of a real person. Registered references are kept while you keep using them and removed once you stop. See the Terms of Service.

Work from a clip

A reference_video is a clip for the model to imitate, edit or extend — motion, pace, framing, or the footage itself.
Fewer models take a clip than take an image, so check supports_video_reference_video first. To raise the resolution of an existing video rather than reimagine it, see Upscale an existing video.

Work from a soundtrack

A reference_audio item gives the model audio to follow — a beat to cut to, a voice to match a mouth to.
Audio is the least widely supported input of the three — supports_video_reference_audio is the flag. Some models also will not take audio as the only media in a request; if one turns this down, send a reference image or a clip alongside it.

Upscale an existing video

Upscalers take a video and return a higher-resolution one. The request is a different shape: the source clip is the only input, and a prompt, ratio, duration or frames are not accepted.
Collect the result exactly as with any other task. A video_url item is required — a prompt on its own returns 400, because there is nothing to upscale. The target resolution is fixed by the model, so resolution does not change the output, but it does pick the per-second price tier you are billed at (720p when omitted).

Finer roles, documents and keyframes

Three roles narrow an ordinary reference role to a slot a few models define. A model without the finer slot still receives the media as the role it narrows, so naming one never costs you a route: reference_document (on a document_url item) narrows nothing: a model that cannot read a document refuses the request rather than generating without it. A reference_image can be pinned to one moment of the video with frame_index (a frame number, counting from 0) or timestamp (seconds from the start), one image per moment. The units are not interchangeable: each reaches only the models that define it, and a model with no keyframe concept still receives the image as an ordinary reference.

What you can send

The ceilings below are the widest any current model allows. A given model may be tighter, so treat a refusal that names a size, a duration or a count as that model’s limit rather than the platform’s. Images — jpeg, png, webp, bmp, tiff, gif, and heic/heif on some models. Each side 300–6000 px, aspect ratio between 0.4 and 2.5, up to 30 MB each. One image for a first frame, two for first-and-last, up to 30 references. Clips — mp4 or mov, H.264/AVC or H.265/HEVC video with AAC or MP3 audio, 24–60 fps. Each side 300–6000 px, aspect ratio 0.4–2.5, up to 200 MB and 2–30 s each, up to 10 clips totalling 30 s. An edit job needs at least 4 s to work from. Audio — wav or mp3, up to 15 MB and 2–30 s each, up to 10 clips totalling 30 s.
The whole request body has a hard 32 MiB ceiling (33,554,432 bytes) on every Mesh endpoint, enforced at the edge — an oversized request never reaches the API and comes back as a plain HTML 413 Request Entity Too Large page rather than a Mesh error. See Rate Limits & Spend Caps.So do not Base64-encode large files: encoding inflates a payload by about a third, and a ~24 MB file already breaks the body limit on its own. Public URLs are the only way to reach the per-file caps above.

Older spellings

Before role existed, the purpose went in type — "type": "first_frame" and friends. Those requests still work and still mean what they meant, so nothing you already send needs changing. Write new ones with the modality in type and the purpose in role: the older form is read as the role it always meant, so an item the model cannot take is refused with a 422 either way. The modern spelling is the one that works everywhere.

Shaping the output

Everything on this page other than model and content is an optional top-level field. Mesh sets no default for any of them — omitted means absent, not “the model’s default”.
A field name this endpoint does not recognise is dropped without a word. There is no 422 and no warning: the request returns 200, the task runs, and your parameter was never part of it. A typo like negitive_prompt, or aspect_ratio where the field is ratio, costs you a whole generation before you notice. Check your spelling against the tables below.Sending a real field to a model that does not implement it is a different case, and that one you do hear about — see What the route dropped.

The basics

Finer control

These shape how the model generates rather than what comes out of the pipe. Support varies by model, which is the whole reason for the capability flags. guidance_scale and num_inference_steps are the only two values Mesh range-checks. Every other field above is forwarded exactly as you wrote it, so the values listed are the ones that mean something to the models implementing that field, not a set Mesh polices. Send output_format: "webm" and you will not get a 422 from us — you get whatever the model makes of an unfamiliar container, which may be an error and may be nothing at all.
negative_prompt is the least predictable of these. Support varies model by model rather than in tidy families — two builds of the same family often differ — which is why it has a capability flag of its own. If a video plainly ignored your negative prompt, read supports_video_negative_prompt before you start rewording it.

Say which reference job you mean

Some models can do several different jobs from reference material: generate something new, edit an existing clip, or extend one. Each job has its own constraints, and if you let the model guess which one you meant, a request whose parameters contradict its guess is accepted and fails minutes later. omni_reference_task_type names the job so the constraints are checked at submit instead: omni_reference_task_type is a Seedance 2.5 parameter (e.g. byteplus/dreamina-seedance-2-5); other Seedance models return 422. The model’s own limits, such as an edit source clip of 4–30 s, are still checked by the model after you submit. Naming the job is worth it even when you are confident: the failure you avoid is the expensive kind to debug.
The model still re-reads your prompt when it runs. If it decides you asked for a different job than the one you named, the task fails anyway — so keep the prompt consistent with the job.

Preview first, then render

draft: true renders a fast, rough version of the same request. When you like one, send its task ID back as a draft_task item and the model renders it properly instead of generating something new from scratch.

Finding out what a model accepts

Video models differ far more than text models do — one takes a closing frame, the next has no concept of one. Two fields answer the question: one before you send, one after.

Check the flags before you send

GET /v1/models returns a capability flag per input, at the top level of each model entry beside supports_tools and the rest. Read them at runtime rather than keeping a list of your own: support genuinely changes between two builds of the same family.
Start with supports_video_generation, which says the model makes video at all. These six then describe what it takes in: Five of the six are named for a content role. The last one is not — it is named for the top-level negative_prompt parameter, and there is no negative_prompt role to put on a content item.
All six read false on every model that is not a video model. An all-false set almost never means “a video model that accepts no input” — it means you are not looking at a video model. Check supports_video_generation first; the other six only mean something once it is true.
A model can be served by more than one route, and the flags can differ between them. GET /v1/models reports the route you are most likely to get; GET /v1/models/{model_id}/providers returns the same six for every route that can serve the model. So the model entry answers “will the route I am likely to get take a last frame”, and the providers endpoint answers “does any route take one”.
The full field list is on Models.

What each model accepts

The flags say WHICH inputs a model takes. This says which VALUES its parameters accept — the resolutions it renders, how long a clip may be, the aspect ratios it offers, and how many media items you may attach. Read it before sizing a request: a value a model does not render is a 422 at submit, and a parameter it does not implement is dropped and named in unsupported_params. 135 models. A dash means the model does not take that parameter, or publishes no value for it.

What the route dropped

A parameter the serving model does not implement is not an error. Mesh takes the request, generates the video without that parameter, and names it in unsupported_params on the response to your POST:
unsupported_params is a flat array of top-level field names, spelled as you sent them, sorted alphabetically. dropped_parameters carries the same drops in the shape chat and image responses use. offering is the model the request was graded against, which is the model that served it if a submit-time fallback moved it. class (advisory or output_semantic) appears when Mesh knows it. Read it before concluding a parameter had no effect: a video that ignored your negative prompt and a video whose negative prompt did nothing useful look identical, and this is what tells them apart. Three things about it will catch you out:
It is on the POST response only. Polling, the list endpoint and the webhook payload do not carry either field, and it is not stored, so if you throw the create response away you cannot get it back. Log it next to the task ID.
It is null, not [], when nothing was dropped. So is dropped_parameters. A truthiness check on .length throws. Guard for null. It names top-level parameters only. model and content are excluded — but a content item the model cannot take is never silently dropped either. It is refused with a 422 at submit, because a dropped asset changes what gets generated and you would be billed for it: a dropped tunable still gives you the video you asked for, tuned differently. Treat a non-empty unsupported_params as something to fix rather than something to live with: the request you meant to send and the request that ran are not the same one.

Getting the result

Two ways to collect a finished video: ask for it, or have it delivered. Polling is the source of truth. A callback is a shortcut that can arrive before your next poll, and on Kling and MiniMax it is skipped if a poll (yours or Mesh’s own background check) settles the task first. You can do both: usage logging is deduplicated, so you are never billed twice.

Poll for the result

GET /v1/video/generations/{id} returns the task’s current state. While a task is still running, Mesh fetches the latest status live before answering; once it has finished, later calls are served straight from Mesh.
A five-second interval is a reasonable place to start. Branch on status, never on the HTTP code: a task that failed still returns 200. Polling a task that has not settled yet triggers a live upstream check, so it needs a key with the video:write scope. A video:read-only key gets 403 there, and can watch progress through the list endpoint instead. An unknown task ID, or one that belongs to another account, returns 404. If your key’s routing policy allows fallback, a submit the first provider rejects can be retried on another provider or model before the task is created. Polling and billing follow the route that accepted it.
You can poll with your own request id instead. Pin X-Mesh-Version: 2026-09 or newer, and the same endpoint also accepts the request_id Mesh returned when you created the task — useful when that id is already what you logged or stored:
The task id is tried first, so nothing you already poll by changes. From 2026-09 the task body also carries a request_id field. Tasks created before 20 August 2026 have no request id recorded and stay reachable by task id only.
A finished task looks like this:
Copy the video into your own storage when the task succeeds. Result URLs are there for you to fetch the file, not to serve it from your product.

Have the result delivered

Pass a callback_url on the create request and Mesh POSTs the task to it when it finishes. Only models served by BytePlus, Kling or MiniMax take a callback_url; on any other model the create call returns 422, and you poll instead. Kling and MiniMax tasks are delivered at most once, on a terminal status (succeeded, failed, expired). If a poll (yours or Mesh’s own background check) settles the task first, no callback is sent. BytePlus forwards every status update it sends, so expect running deliveries too and act on the terminal one. Treat the callback as a shortcut and polling as the source of truth.
The body is the same shape polling returns, with one difference: null fields are omitted rather than sent as null. Read a missing key as null, not as an error. To hear about every video task on the account without a per-request URL, subscribe to the video.completed and video.failed webhook events.
Return a 2xx quickly, and do the heavy work afterwards. The callback is fire-and-forget with a 10-second timeout and no retry — a slow or failing endpoint means you miss that event for good. Poll as a backstop if missing one would matter.

List your recent tasks

GET /v1/video/generations returns your tasks, newest first. It is served from Mesh without checking upstream, so an in-progress task can show a stale status — call the single-task endpoint for a live one.

What a video costs

How a video is billed depends on the model’s pricing.pricing_unit on GET /v1/models: Where a model prices them separately, audio (generate_audio: true) and video input change the rate, not always upward. On Seedance 2.x, generating from a video input is billed at a lower rate per token than video alone. Venice models bill the price Venice quotes for the shape you asked for. billed_usd on the finished task is what you were charged, and billed_resolution the tier it was charged at. Per-model rates are on the Pricing page, and Models & Pricing carries the live numbers.

When something is refused

A task that failed after it was accepted

The most common surprise here: a video task can be accepted and fail later. The GET returns HTTP 200 with status: "failed" and an error object — the failure is in the body, not the status code.
So always branch on status, and log error.code. Mesh’s own codes are: Any other code is the provider’s own, passed through as it sent it; message carries the provider’s explanation.

Where to look first

HTTP status reference

429, 502 and 503 are usually transient — retry with backoff. For anything else, Video generation troubleshooting walks through the specific symptoms, and the error reference covers codes shared across the API.