Timeline invalid_fps 400: output.fps must be 24, 25, 30 or 60
invalid_fps rejects any output.fps outside 24, 25, 30 and 60. Omit the field to match the source frame rate; details.allowed lists the accepted values.

invalid_fps is a 400 from Timeline 1.0 when output.fps is not one of 24, 25, 30 or 60. A value such as 23.976 or 29.97 fails, because the field is an enum of whole numbers. The body points at the field and lists the accepted values, and its next_action is use_supported_fps.
The error body
Sume rewrites the raw schema failure into a stable shape, so you can read it without parsing validator output.
| Field | Value |
|---|---|
| message | output.fps must be one of: 24, 25, 30, 60. |
| details.field | output.fps |
| details.received | The value you sent |
| details.allowed | [24, 25, 30, 60] |
| details.next_action | use_supported_fps |
The better fix is to leave it out
The Timeline docs say that if you omit output.fps, the job renders at the rate of the sources: the longest video sources decide, stills have no rate, and 30 applies only when no source has a rate. Picking a rate that differs from a source makes the job repeat or drop a frame every few frames, which shows as judder, and the job reports output_fps_resamples_sources with the rate the sources wanted. So for fractional-rate footage the cleanest answer is usually no fps at all.
Pick the nearest allowed rate
If you must set one, map the source rate to the closest allowed value, and prefer the one your delivery platform expects. This runs as is:
ALLOWED = (24, 25, 30, 60)
def nearest(src_fps: float) -> int:
return min(ALLOWED, key=lambda a: abs(a - src_fps))
for f in (23.976, 29.97, 50, 59.94):
print(f, "->", nearest(f))Use the free plan call first
POST /v1/timeline-1.0/plan runs the schema checks and the compiler without creating a job or reserving credits, and Idempotency-Key is not required. Send your body there first, and an invalid_fps costs you nothing. The render itself requires an Idempotency-Key.
Common sources of a bad rate
Most invalid_fps errors come from copying a number out of a probe. Phone footage often reports 29.97 or 59.94, screen recordings can report odd variable rates, and a template may hard-code 29.97 from a broadcast habit. None of these is in the enum. Read the rate from your source only to decide, not to pass through, and prefer to omit the field so that the job follows the sources.
If the clip goes to a short-form platform, 30 is a safe and common target, and 24 suits film-like footage. For a mix of sources with different rates, omitting output.fps lets the longest video source decide, which is the documented rule.
Checklist
Before you release a template: drop output.fps unless a platform needs a fixed rate; if it does, store one of 24, 25, 30 or 60; run the plan call in CI; and read the output_fps_resamples_sources note in a test render, to see whether the choice makes the job resample.
Limits
The plan call cannot predict the short-source pad or loop warnings. The full field table is in the Timeline docs; the related post covers a 23.976 fps clip in detail.
Sources
Related posts
More in Developers
- Timeline invalid_transition_type: why xfade is rejected (use fade)
transition.type accepts fade, wipeleft, wiperight, slideup, slidedown and dissolve. xfade is the internal ffmpeg name, so it returns invalid_transition_type.
- Is Luma Ray3.2 on Sume? Check the video catalog ids in Python
Ray3.2 is not a Sume model. Rather than trust a blog, list GET /v1/video-router/models and test for an id. A short Python script that prints every id.
- Java 21: poll many Sume jobs with virtual threads and a Semaphore
Poll 50 Sume jobs from one Java 21 file using virtual threads, a Semaphore cap of 8, and java.net.http. No Maven, no dependencies, runs with java Poll.java.
- Java ImageIO.read returns null on a Sume WebP: request PNG
ImageIO.read gives null when no reader claims the stream. Check the reader list, then pin output_format to png or jpeg on a Sume /v1/images call.
Written by Sume