We were billed for 720p and got a 360p file.
Not a rounding error, not a fallback, not a documented limitation — the resolution parameter on that endpoint has no effect at all. We only found out because of a boring rule we'd adopted for unrelated reasons: when you record a media file's properties, record what you measured, never what you requested.
That rule costs nothing. It also turned out to be the only reason we noticed.
What we did
Every row below is a real generation through fal, paid for, with the returned file read back by ffprobe:
ffprobe -v error -show_entries format=duration \
-show_entries stream=width,height \
-of default=nw=1 delivered.mp4
Not the API response's metadata. Not the request we sent. The file.
| Model | Operation | Requested | Delivered | Honoured | Generations |
|---|---|---|---|---|---|
| PixVerse v6 | extend | 720p (source 640×360) | 640×360 | ❌ | 1 |
| PixVerse v6 | extend | 540p (source 1280×720) | 1280×720 | ❌ | 1 |
| Pikaframes | keyframe transition | 720p · 7s | 1280×720 · 7.083s | ✅ | 1 |
| Pikaframes | keyframe transition | 720p · 5s | 1280×720 · 5.083s | ✅ | 1 |
| Pikaframes | keyframe transition | 1080p · 5s | 1920×1080 · 5.083s | ✅ | 1 |
| Pikaframes | keyframe transition | 1080p · 10s | 1920×1080 · 10.042s | ✅ | 1 |
| Kling 3.0 Pro | image to video | 1080p · 5s | 1920×1080 · 5.042s | ✅ | 4 |
(The durations land a few frames long because encoders emit whole frames. 7.083s at 24fps is 170 frames. That's not a broken parameter, that's arithmetic.)
Seven rows. That's the whole table — every row is a generation somebody paid for, and each carries a pointer back to the run it came from. There is no row for a configuration nobody ran.
One measurement would have given us the wrong answer
The first observation was the 360p one, and the obvious reading was "this endpoint won't upscale" — a sane, common limitation. We wrote that down and moved on.
It was wrong, and the way we found out was running the other direction: a 1280×720 source asking for 540p. If the endpoint merely refused to upscale, a downscale request should have worked.
It came back 1280×720.
So the parameter isn't a floor, isn't a ceiling, isn't a clamp. It's inert. And the second case is worse than the first: the file is larger than what was paid for, and it's labelled with a resolution it doesn't have.
"It won't upscale" and "the parameter does nothing" produce identical evidence from a single upscale test. The first sounds like a reasonable product decision, which is exactly why nobody questions it.
Why recording measured values catches this for free
If you write the request into your database:
{ resolution: '720p' } // what you asked for
you will never discover this. Your records will agree with themselves forever.
If you write what came back:
{ width: 640, height: 360 } // what ffprobe read
the discrepancy shows up the first time it happens, in normal operation, with no test to write and no test to remember to run.
This is the part I'd push on if you take one thing from this post: it isn't a testing strategy, it's a recording strategy. The check is a side effect of storing the honest value. We adopted it to make our billing records accurate, not to audit vendors, and it audited a vendor.
Same shape applies well beyond video. Any time you store a parameter you sent instead of the property you received — image dimensions, audio sample rate, token counts, page sizes — you've built a system that can't notice being lied to.
What this doesn't say
- These are small samples. Ten generations across seven configurations, not a sweep. They're paid, so the table grows when verification work happens rather than from a benchmark run.
- While drafting this I nearly published a number nobody measured. An internal summary listed a model's 1080p·10s result but not its 1080p·5s, so I filled the gap with a value that matched the pattern: 5.042s. The real one is 5.083s. The tier was real, the run had happened, only my number was invented — and it was 41 milliseconds off, sitting next to a genuine 5.042s from a different model. That's harder to catch than a made-up row, because everything around it checks out.
What fixed it wasn't being careful. Every row in the table now has to name the run it came from, and the invented one couldn't: for 5.042s there was no run to point at. A gate can't tell you a number is wrong, but it can refuse numbers with no origin.
-
Each row is about one operation on one model. The PixVerse rows are about
extendand only aboutextend. We have not measured that provider's other endpoints, and nothing here should be read as a claim about them. The same caution runs the other way: Pikaframes honouring its parameters on keyframe transitions says nothing about its other modes. - We're not claiming intent. An inert parameter is much more likely to be a bug than a scheme.
What we changed
The endpoint now refuses any extend whose source tier we don't have a price for, instead of accepting it and returning a file labelled with a resolution it doesn't have. A 1080p source can no longer be extended at all on our side. That's a real capability we gave up.
The alternative was to add 360p and 1080p rates and bill by whatever came back. We didn't, because the quote is shown before the user commits, and a quote you can't compute accurately is a promise you can't keep. Refusing is worse product and better arithmetic.
We keep the measured numbers up to date at ai-video-generators.com as we run more verifications. If you've measured something that contradicts a row above, I'd genuinely like to know — the table is only useful if it's correctable.
Top comments (0)