Video filter 400 video_filter_ops_empty: send one op or a filtergraph
Video filter 1.0 needs at least one op in ops[] (max 8) or a non-empty filtergraph. POST /v1/video-filter/check returns diagnostics for free before you pay.

video_filter_ops_empty is a 400 that says "Video filter 1.0 needs at least one op in ops[]." and carries next_action: add_filter_op. It fires when ops is missing, empty or not an array. The Video filter docs say a program needs at least one op or a non-empty filtergraph, so the other way out is to send a filtergraph instead.
The program rules
Video filter 1.0 applies ops[] first and filtergraph after. Each refusal has a stable public_reason that names the failure, so you do not get a generic invalid_request:
| public_reason | Cause | Limit or fix |
|---|---|---|
| video_filter_ops_empty | No ops and nothing else to run | Add an op, or send a filtergraph |
| video_filter_too_many_ops | More ops than allowed | At most 8 ops per job |
| video_filter_amount_out_of_range | dim amount outside (0, 1] | 0 and values above 1 are refused |
| video_filter_crop_out_of_bounds | Crop rectangle leaves the frame | x+width <= 1 and y+height <= 1 |
| invalid_filtergraph | Empty or too long filtergraph | At most 2048 characters, filters only |
| ffmpeg_fields_rejected | Fields like codec or crf | Not part of the public contract |
Check first, for free
POST /v1/video-filter/check runs the same schema, whitelist and source checks as the encode, but it returns diagnostics and not a 400. It creates no job, reserves no credits and does not use the encoder. A valid response has object: video_filter_check, valid, diagnostics[] and next_action of submit_video_filter or fix_program_and_recheck. It does not need an Idempotency-Key; the encode does.
A check call that runs
This sends one dim op. It needs a clip that already lives on media.sume.com for your workspace, because the API does not fetch from the open internet, so import the file first. Replace the URL with yours:
import json, os, urllib.request
key = os.environ.get("SUME_API_KEY")
if not key:
raise SystemExit("set SUME_API_KEY")
body = {"video_url": "https://media.sume.com/artifacts/artf_demo/talk.mp4",
"ops": [{"op": "dim", "amount": 0.45}]}
req = urllib.request.Request("https://api.sume.com/v1/video-filter/check",
data=json.dumps(body).encode(), method="POST",
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
try:
out = json.load(urllib.request.urlopen(req))
print(out.get("valid"), out.get("next_action"))
except urllib.error.HTTPError as e:
print(e.code, json.load(e).get("error", {}).get("code"))What a passing check does not promise
The docs say a program that passes the check can still fail on the worker, for example on a bad expression, memory or time, and then you get a structured job error. The check also does not predict that. Public pricing is listed in the docs as a flat rate per encode job, and the live number is in GET /v1/catalog; the check itself is free.
See the Video filter page for the whole field table, and keep the request_id of any refusal.
Common ways to hit the empty error
The usual causes are a templating bug that renders ops as null, a UI that lets the user remove the last op and still submit, and a client that treats a no-op as a valid program. In each case the check endpoint returns the diagnostic at no cost, so put it behind the submit button and let your own code stop an empty program before any call.
Sources
Related posts
More in Developers
- video_inspect: fps 2 and a 24-still cap cover only 12 seconds
At fps 2 video_inspect's 24-still cap covers 12 seconds. Pick fps or explicit at[] times by clip length; a table of coverage from 12 s to 96 s per call.
- video_inspect seek fast vs precise: a quick look with keyframe stills
seek: fast moves each still to the keyframe at or before its instant and skips the decode. Use it for a quick look; precise when timing matters.
- Video job statuses: pending, cancelled vs Sume's five job states
A /v1/videos job says pending or in_progress; /v1/jobs/{id}/status says queued or processing. Here is the mapping, which one to poll, and the spelling trap.
- Video model fallback chain in Python with one key per model
A Python chain that tries Gemini Omni, Wan 3.0 and Seedance 2 on Sume in order, stops on 401 or 402, and uses a separate idempotency key for each model.
Written by Sume