Image and Video API Input Validation: Why Models Reject Media
Why models reject valid-looking media, and the input checks that catch it before the API call.

The request can be valid JSON. The image can open normally on your laptop. The video can play in a browser.
The model can still reject it.
One version of this failure is especially unhelpful:
Invalid image(s) found in the input, failed to read/process.
An image or video input usually fails at one of four layers: the request does not match the target model schema, the file cannot be retrieved, the media cannot be decoded, or the asset violates a model-specific constraint such as resolution, aspect ratio, file size, duration, or encoding.
Those are different failure classes. Debug them separately.
A model demo also gives you a cleaner input distribution than production ever will. Real users send screenshots, camera originals, exports, expired URLs, odd aspect ratios, oversized media and unusual encodings.
Canberk's POV: A model working is not the same as the product working. Validation, preprocessing and useful failure states are part of the product around the model call.

Input validation has four different layers
“Invalid input” is too broad to be a root cause.
| Validation layer | Typical problem | What to inspect |
|---|---|---|
| Request schema | Missing field, wrong field name, invalid type or enum | The target model's request_schema |
| Media retrieval | Private, expired or otherwise unreachable asset | Whether the URL actually returns the intended media |
| Media decoding | Corrupt file, unexpected bytes, unsupported encoding | Actual media metadata and decoder result |
| Model constraint | Invalid dimensions, ratio, size, duration, format or another model limit | Current model requirements |
The first layer concerns the API request. The other three concern the asset behind it.
Something like this can therefore be perfectly valid JSON:
{
"image_url": "https://example.com/input.png",
"aspect_ratio": "16:9"
}
and still fail later. You still do not know whether the target model actually uses image_url, whether 16:9 is valid for that model, whether the asset can be fetched, whether the bytes decode as a PNG, or whether the model has another media constraint outside its JSON request schema.
On each::labs, a 400 input-validation failure is separate from transient server failures. The Error Reference describes Invalid input as an input that does not match the model's request schema.
Start there.
Start with the model schema, not the error message
Models that do similar jobs often accept different request structures.
One might use image_url. Another expects image_urls. Another uses image_url_1.
The same thing happens with geometry controls. One model exposes resolution; another uses image_size.
Do not smooth those differences away by guessing.
Fetch the current request_schema
each::labs exposes the model contract through its metadata. The preferred endpoint for one model is:
GET /v1/models/{slug}
The model-catalog endpoints are public, so you do not need an API key just to inspect a schema.
The response includes the current model version and its request_schema. See the Get Model documentation and List Models documentation.
The schema can establish:
- required properties;
- JSON types;
- allowed enum values;
- numeric or string bounds encoded in the schema;
- current parameter names.
That lets you validate the request before inference instead of keeping a second, handwritten version of the contract in your application.
import Ajv from "ajv";
const slug = "veo3-1-image-to-video";
const response = await fetch(
`https://api.eachlabs.ai/v1/models/${encodeURIComponent(slug)}`
);
if (!response.ok) {
throw new Error(`Could not load model schema: ${response.status}`);
}
const model = await response.json();
const input = {
prompt: "A product rotating slowly on a studio table",
image_url: "https://example.com/product.png",
duration: 8,
resolution: "720p",
aspect_ratio: "16:9"
};
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(model.request_schema);
if (!validate(input)) {
console.error(validate.errors);
throw new Error("Input does not match the target model schema");
}
const predictionPayload = {
model: slug,
version: model.version,
input
};
This catches wrong parameter names, invalid enums, missing required values and incorrect JSON types before the generation call.
It still does not prove that product.png is usable media.
What request_schema cannot tell you
JSON Schema describes the request contract. It does not inspect the file itself.
A schema cannot, by itself, prove that:
- the URL still resolves to the intended asset;
- the image or video bytes decode correctly;
- every binary-media constraint is represented in the schema;
- a particular video stream encoding is accepted;
- a particular color profile, bit depth or alpha configuration is accepted.
Two models can both accept an image URL string and apply different rules to the image behind it.
Normalizing syntax does not normalize media semantics.

A valid URL is not necessarily valid media
Once the request matches the schema, inspect the path from the URL to the asset.
Can the service retrieve the file?
A syntactically valid URL can still be useless downstream.
The asset may require authentication. A signed URL may have expired. The endpoint may return a different response from the one you saw in your browser.
Test retrieval from your backend rather than assuming another service can fetch a file because it opens in your current session.
each::video makes this boundary explicit. Its staging layer accepts publicly downloadable media URLs and rejects inputs it cannot stage because they are unreachable, non-public or not identifiable as media. Its exact extension and mime_type= rules are specific to each::video; they are not a universal contract for every generation model.
Canberk's POV: Errors that tell you what to do next are a product feature. Integration failures collect at boundaries such as storage, permissions, media metadata, request schemas and downstream model behavior.
Upload success does not mean model success
This is particularly important with each::storage.
Storage and inference have separate contracts.
each::storage currently permits files up to 100 MB per upload. A successful upload proves that storage accepted the asset and gives you a public_url that can be passed to a model.
It does not certify that the model will accept it.
A target model may have a lower file-size limit, different format rules, a duration bound or another media restriction.
Upload success is not model validation.
Check the bytes, not just the extension
A filename ending in .jpg does not prove there is a usable JPEG behind it. The file may be truncated, corrupt or simply fail to decode.
Video adds another distinction. MP4 describes a container. It does not tell you which video and audio codecs are inside it.
When a video fails media processing, probe it:
ffprobe \
-v error \
-show_entries format=format_name,duration,size:stream=index,codec_type,codec_name,width,height \
-of json \
input.mp4
Now you are looking at the asset rather than its filename.

Check the media itself: resolution, aspect ratio, size, duration, and encoding
Once the request, retrieval path and decoder are known to work, compare the media against the target model's requirements.
There is no universal image or video contract across generation models.
Resolution and dimensions
“Resolution” can describe different things at different layers.
A request may expose an enum such as 720p or 1080p, while the source asset has raw dimensions such as 1920 × 1080.
They are related, but they are not the same contract field.
Inspect the actual width and height, then compare them with the model's documented minimum, maximum or preset rules. Do not assume that a model capable of producing a particular output resolution accepts source media at any resolution.
Large files are not automatically safer either. If a model has an upper bound, an oversized source is just as invalid as an undersized one.
Aspect ratio
Aspect ratio creates two separate questions:
- Which ratio values can the request specify?
- Which source-media shapes can the model consume?
If aspect_ratio appears as an enum in request_schema, the first question is straightforward. The second may not be encoded there.
Some models transform the source to fit their output framing. Others impose their own source-media restrictions. Unless the contract says otherwise, do not assume the input must exactly match the requested output ratio.
For a 1920×1080 image:
const ratio = 1920 / 1080;
// ≈ 1.7778, or 16:9
“Landscape” is not a validation rule. The exact model contract is.
File size
A single application can have several file-size ceilings:
- your application's upload limit;
- the storage limit;
- the model's own media limit;
- a request-body limit when media is embedded instead of referenced.
Those are different numbers.
For example, the current openai-image-edit page on each::labs states that input images must be PNG or JPEG and under 50 MB, while each::storage accepts uploads up to 100 MB.
A successful upload therefore cannot stand in for model validation.
Video duration
A video can decode correctly and still violate a deterministic duration rule.
Measure duration before submission whenever the target operation or model defines a bound.
each::video does this during pre-compute validation: it probes source duration and rejects inputs over its documented cap before compute. That limit belongs to each::video, not every generation model, but the architectural lesson carries over.
If duration is invalid, sending the same file again will not change anything.
Format, container and codec
For images, format is only the first media check.
For video, distinguish between:
- the container, such as MP4, MOV or WebM;
- the video codec inside it;
- the audio codec, where audio matters.
Do not invent codec restrictions when the model docs do not specify them. But if a video has the expected extension and still fails processing, inspecting its streams is more useful than retrying it unchanged.
Color profile, bit depth and alpha
This category needs more caution than most validation checklists give it.
There is no current each::labs-wide rule saying every image must be sRGB, that all CMYK files are rejected, or that one ICC profile is valid across every model.
Do not turn this into a blanket preprocessing rule such as “convert every input to sRGB.”
Treat color space, bit depth, channel configuration and alpha as properties to inspect when the target model documents a requirement, or when an otherwise valid file keeps failing media processing.
A model-specific requirement is evidence. A platform-wide assumption is not.
Model-by-model input checklist
The point of this table is not to create a permanent compatibility database. It is to show how different the contracts can be, then send you back to the live schema for the model you are actually calling.
| Model | API slug | Request-level checks | Media-level checks |
|---|---|---|---|
| Sora 2 Image to Video | sora-2-image-to-video |
Current request surface includes image_url, aspect_ratio and duration |
Current model page documents JPEG/PNG/WebP input, up to 20 MB, and exact source-resolution matching to the target video dimensions. Verify the live schema before hard-coding request values. |
| Veo 3.1 Image to Video | veo3-1-image-to-video |
Current request surface includes image_url, duration, resolution and aspect_ratio |
Prefer the live request_schema for API validation; the current editorial page contains broader capability descriptions than the concrete request example. |
| OpenAI Image Edit | openai-image-edit |
Uses image_url_1 and image_size rather than the singular field pattern used by some other editors |
Current model page states PNG/JPEG input under 50 MB and says WEBP is unsupported. |
| GPT Image v1.5 Edit | gpt-image-v1-5-edit |
Uses image_urls plus image_size, quality, input_fidelity and output controls |
Validate every referenced image in the array. Media constraints not present in the request schema still need separate verification. |
The important part is not which field name appears most often. It is that the contracts differ.
A validator written only for input.image_url does not validate a model that expects input.image_urls. The same applies to resolution, duration and file-size assumptions.
For a concrete model-specific troubleshooting example, see How to Access Sora 2 via API.
Build a preflight check before generation
The right time to discover a deterministic media problem is before inference starts.
A preflight does not need to predict whether the output will be good. It only needs to catch conditions that already make the request invalid.
target model
↓
fetch request_schema + version
↓
validate request object
↓
retrieve media
↓
decode / probe media
↓
inspect dimensions, bytes, duration and streams
↓
compare with authoritative model-specific limits
↓
submit prediction
1. Validate the request object
Fail before inference when a required property is missing, the wrong field is present, a type is wrong, or an enum value is unsupported.
An error like:
aspect_ratio must be one of the values accepted by this model
is more useful than:
invalid request
2. Retrieve and inspect the media
For image preflight, useful metadata can include:
{
contentType,
byteSize,
width,
height,
format,
channels,
colorSpace
}
For video:
{
contentType,
byteSize,
container,
videoCodec,
audioCodec,
width,
height,
duration,
hasVideoStream,
hasAudioStream
}
The libraries are an implementation detail. What matters is validating the bytes your backend actually received rather than trusting a filename or client-supplied MIME type.
If your decoder cannot open an image, stop there. Do not send it to a slower, paid generation path and hope the downstream decoder is more forgiving.
3. Compare metadata with authoritative constraints
Only enforce a model constraint when you have a source for it.
If request_schema contains:
{
"aspect_ratio": {
"type": "string",
"enum": ["16:9", "9:16"]
}
}
then those request values can be validated deterministically.
If the schema says nothing about ICC profiles, it does not justify inventing an ICC rule.
If current model documentation defines a file-size ceiling or accepted format outside JSON Schema, keep that rule with the model-specific configuration. Do not mix it with your own product preferences.
There is a real difference between:
- the model requires this, and
- our product chooses to normalize this.
Your errors should preserve that distinction.
4. Return actionable failures
Instead of:
{
"error": "invalid_media"
}
prefer something that identifies the failed boundary:
{
"error": "media_unreachable",
"message": "The supplied image URL could not be retrieved."
}
or:
{
"error": "unsupported_aspect_ratio",
"message": "This model does not accept the requested aspect ratio.",
"model": "veo3-1-image-to-video"
}
Errors that tell you what to do next are a product feature.
A generic validation layer does not remove model differences. If it hides them, it pushes those differences into failures that take longer to diagnose.

Fix deterministic failures. Retry transient ones.
An input that violates the model contract will not become valid because you send it twice.
| Failure | Retry unchanged? | Correct response |
|---|---|---|
| Missing/wrong request field | No | Fix the request |
| Invalid enum or parameter value | No | Use a supported value |
| Unsupported dimensions | No | Resize or replace the input |
| Unsupported aspect ratio | No | Change the source/request as required |
| File too large | No | Reduce or replace the asset |
| Video too long | No | Trim or replace the video |
| Unreadable/corrupt media | No | Re-encode or replace the file |
| Private/unreachable URL | No | Fix media access |
| 429 concurrency cap | Not immediately | Wait for an in-flight execution to settle; use bounded retry where appropriate |
| 500 server error | Potentially | Retry with exponential backoff |
Canberk's POV: Routing can help with infrastructure failure. It cannot make malformed media readable or make an unsupported request valid.
Sending the same bad input to another model without checking that model's contract is not recovery. It is another unvalidated request.
Use the current Error Reference to separate deterministic input failures from conditions that may succeed later.
Generated outputs need validation too
User uploads are not the only source of invalid inputs.
A file created successfully by one model can still fail at the next step. Model B may expect a different media field, aspect ratio, dimension range, file-size constraint, format or reference-image contract.
“Generated successfully” means the previous step completed. Nothing more.
If you are chaining generation, upscaling and video, inspect the intermediate artifact before each handoff. The same boundary problem is covered in more depth in How to Chain Image, Upscale, and Video in One API Call.
A practical order of operations
The next time an image or video is rejected:
- Identify the exact model.
- Fetch its current
versionandrequest_schema. - Validate field names, required values, types and enums.
- Verify that the referenced media is retrievable.
- Decode or probe the actual bytes.
- Check width, height and aspect ratio.
- Check byte size and video duration where relevant.
- Inspect format, container, streams and documented model-specific restrictions.
- Fix deterministic violations before submitting again.
- Retry only when the failure class is actually transient.
That procedure is more durable than starting from the wording of an error message. Error messages change. The validation layers do not.
FAQ
Why does an image API say an image is invalid even though it opens on my computer?
Your local image viewer and the target model do not necessarily enforce the same contract. The request may contain a wrong field or value, the downstream service may be unable to retrieve the URL, the bytes may fail its decoder, or the image may violate a model-specific requirement.
Start with request_schema, then inspect the actual media.
Does .jpg, .png, or .mp4 guarantee compatibility?
No.
An extension does not prove the file can be retrieved or decoded. In video, the container name also does not fully describe the codecs and streams inside it.
Can each::storage accept a file that a model rejects?
Yes.
each::storage currently permits uploads up to 100 MB, but storage acceptance and model acceptance are different checks. A successful upload does not certify the downstream inference request.
Should I retry an invalid-input error?
Not unchanged.
A wrong field, unsupported value, unreadable file, invalid dimensions, excessive duration or another deterministic constraint will normally fail until the request or media changes.
For current each::api behavior, 500 errors are retry candidates. A 429 generally represents an account concurrency cap, so let an in-flight execution settle and then retry as appropriate rather than assuming backoff alone solves it.
How do I find the accepted parameters for an each::labs model?
Retrieve the target model and inspect its request_schema.
The response also includes the current model version. Use both when constructing the prediction instead of hard-coding a stale contract.
Do I need to convert every image to sRGB?
There is no current each::labs-wide rule establishing that every model requires sRGB.
Treat color space, bit depth, alpha and related encoding properties as model-specific constraints. Inspect them when the target model documents a requirement or when an otherwise valid file continues to fail media processing.
Canberk Sinangil
Co-founder & CTO, each::labs
I’m the Co-Founder and CTO of each::labs, where I focus on building the infrastructure and tooling that help developers bring AI models into production. My background spans computer vision, augmented reality, machine learning, and software engineering, including building AR and visual AI products before moving deeper into generative AI. I’m particularly interested in the engineering challenges behind making powerful AI models fast, scalable, and practical for real-world products.