video-filter invalid_filtergraph: every reason and the fix for each
A video-filter filtergraph is refused for eight reasons, from a [0:v] label to a quote character. What each one means and how to rewrite the graph so it passes.

invalid_filtergraph on Sume video filter means the filtergraph string failed the contract check before any encoder started. The check is a pure string check, so you can reproduce it for free with POST /v1/video-filter/check, which returns diagnostics instead of a 400. The reason is one of eight, and the fix is almost always to delete something you copied from an ffmpeg command line.
The eight reasons
The server wraps your graph itself as [0:v] ... [vout], so the string you send is filters only. The validator in packages/timeline-compiler/src/filter.ts reports these reasons. The table pairs each with a typical cause.
| Reason | What trips it | Fix |
|---|---|---|
| empty | Blank or whitespace-only string | Send at least one filter, or use ops[] instead |
| too_long | More than 2048 characters | Move crop and dim into ops[], shorten expressions |
| too_many_filters | More than 32 named filters | Split the work across two jobs |
| forbidden_token | ://, a backslash, backtick, $, double quote, &, newline or tab; or whitespace inside one filter | Use single quotes only, write options as name=key=value:key=value with no spaces |
| input_reference | Graph starts with [, or a label is [0:v], [1:a] or a bare index | Drop the input label; the server supplies it |
| output_label | Graph ends with ] | Drop [out]; the server maps the last filter |
| unbalanced | Unclosed parenthesis or quote | Count them; commas inside (...) or '...' are protected |
| unknown_filter | A name that is not on the allowlist, or text that is not a filter token | Check the allowlist below |
Three rewrites that come up most
Pasting a full ffmpeg command is the first. A string such as -vf scale=1080:1920 -y out.mp4 fails because -vf is not a filter token, and the API rejects vf, filter, ffmpeg, cmd and codec as request fields as well (ffmpeg_fields_rejected). Send only scale=1080:1920.
Labels are the second. [0:v]scale=1080:1920[vout] fails twice, on the leading specifier and on the trailing output label. Internal labels such as [a] and [b] are fine inside split, overlay and hstack chains, because those need them to route one clip to two branches. Anything that starts with a digit or contains a colon is treated as a stream specifier.
Filter names are the third. drawtext, subtitles, movie and lut3d are not on the allowlist, since they read files or fonts. For burned-in text use video captions with cues. trim and setpts left the list when video trim shipped, so cut ranges with video trim.
What is on the allowlist
The docs describe the list as tone, blur, geometry, fade and internal compositing filters, and the source names them. Geometry includes crop, scale, pad, hflip, vflip, transpose, rotate, setsar, setdar, zoompan; time includes fps, tpad, fade, framerate, tblend, tmix; compositing includes split, overlay, hstack, vstack, blend, drawbox, drawgrid, format. The unknown-filter message prints the full list, so one failed check gives you the current one.
Check, then submit
Run the same body against the check route. A valid graph returns valid: true, encode: "not_run", the compiled filter names and an estimate. An invalid one returns diagnostics with the offending token. Encode jobs are $0.02 each and the check is free; confirm the live rate in GET /v1/catalog. A graph that passes can still fail on the box for a bad expression or memory, which comes back as a structured job error.
curl -X POST https://api.sume.com/v1/video-filter/check \
-H "Authorization: Bearer $SUME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://media.sume.com/artifacts/artf_demo/talk.mp4",
"filtergraph": "scale=1080:1920,eq=contrast=1.05"
}'Sources
Related posts
More in Media tools
- video-filter unsupported_filter_op_field: crop and dim keys allowed
A crop op takes four fractions and dim takes one amount; any extra key returns unsupported_filter_op_field. The exact key lists and where the extra work goes.
- video-frames returned a null url: the job succeeded, one frame failed
If one instant fails to extract, video-frames sets that frame's url to null and the job still completes. How to detect it and retry only that time.
- video-inspect frames: at[] and fps together is a 400, pick one
video-inspect and video-frames take a list of times or a sample rate, never both. The codes, the 24-still cap, the fps 2 ceiling, and the empty object.
- Inspect a 3-minute Short with a transcript: set duration_seconds 180
video-inspect reserves one minute of speech-to-text unless you send duration_seconds (max 600). Why a 180 hint matters, the $0.01 per minute rate, and the 400s.
Written by Sume