'first_frame_url must match image_url': two names, one value
image_url and first_frame_url are aliases on Video Router. Send both only if the strings are identical, same for end_image_url and last_frame_url, or get a 400.

On Video Router, image_url and first_frame_url name the same first frame, and end_image_url and last_frame_url name the same last frame. If both names of a pair are present and the two strings differ, the request is refused with "first_frame_url must match image_url when both are provided." or its last-frame twin. Send one name per pair.
The rule, exactly
The check compares the two strings for equality. Two URLs that point at the same picture but differ in a query string, a trailing character or a host alias are different strings and fail. The code does not fetch either URL for this comparison. Where the check fails, the error path is the second name, first_frame_url or last_frame_url, so a client that highlights the offending field will point at the alias rather than at image_url.
| Pair | Both present and equal | Both present and different |
|---|---|---|
| image_url, first_frame_url | Accepted | 400: first_frame_url must match image_url when both are provided. |
| end_image_url, last_frame_url | Accepted | 400: last_frame_url must match end_image_url when both are provided. |
| Only one name of a pair | Accepted | Not applicable |
Why two names exist
The first-frame and last-frame names mirror the frame_type values of the OpenRouter-shaped route, first_frame and last_frame, so a client written against that vocabulary can keep its spelling. The older image_url and end_image_url names were the original flat fields. The normalized request takes whichever is present, preferring image_url and end_image_url.
Pick one name in your client
The simplest fix is to choose a spelling and delete the other from your request builder. The snippet refuses a conflicting pair locally and keeps one key, so you see the problem before a round trip. It prints the final body and sends nothing.
def one_name(body: dict) -> dict:
out = dict(body)
for keep, alias in (("image_url", "first_frame_url"),
("end_image_url", "last_frame_url")):
a, b = out.get(keep), out.pop(alias, None)
if a is not None and b is not None and a != b:
raise ValueError(f"{alias} differs from {keep}")
if a is None and b is not None:
out[keep] = b
return out
print(one_name({
"model": "kling-3",
"prompt": "A door opens onto a garden",
"first_frame_url": "https://example.com/door-closed.png",
"end_image_url": "https://example.com/garden.png",
"last_frame_url": "https://example.com/garden.png",
}))
Limits
The pair rule says nothing about whether the model takes an end frame at all. Catalog capabilities decide that: grok-imagine-video-1.5 and h3-max-recast have no end frame, while kling-3 and the Seedance and Wan models do. Read capabilities from the model endpoint first. Also keep the URLs public HTTPS, since the frame fields use the public remote URL schema.
A last practical note: normalize URLs before you compare them. If one layer signs a URL and another strips the signature, the strings drift apart without anyone changing the picture, and the 400 looks mysterious. Build the request from one variable per frame and the problem cannot occur. If you must accept both names from outside callers, run the check in your own code so the error message reaches the person who typed the URLs, not a log nobody reads.
Sources
Related posts
More in Developers
- Fix a first frame with Ideogram 4.5, then send it to Omni image_url
Correct text or a prop in a still with Ideogram 4.5 on Sume, then use the edited image as the Gemini Omni 1.1 Flash first frame. Two jobs, one shot.
- Flare vs Sunburst A/B test: 100 prompts cost $3.30 at medium
A 100-prompt A/B test of GPT Image 2.5 Flare and Sunburst costs $3.30 at medium on Sume, $13.18 at high. Budget by tier, plus a cost-logging script.
- FLUX 3 Image 503: read the JSON status before you retry
BFL says a FLUX 3 Image 503 may be retryable once you check the JSON status; 422, 400 and 402 are not. Sume sync failures return a 502 with a retryable flag.
- FLUX 3 Image 0-1000 boxes to pixels on a 1920x1080 canvas
FLUX 3 Image boxes are [top, left, bottom, right] on a 0-1000 grid. The BFL example [250, 50, 850, 650] becomes x 96, y 270, 1152x648 pixels on 1920x1080.
Written by Sume