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.

4 min readSume
All posts

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:

Video filter refusals and limits (Sume Video filter docs and API source, read 2026-10-05)
public_reasonCauseLimit or fix
video_filter_ops_emptyNo ops and nothing else to runAdd an op, or send a filtergraph
video_filter_too_many_opsMore ops than allowedAt most 8 ops per job
video_filter_amount_out_of_rangedim amount outside (0, 1]0 and values above 1 are refused
video_filter_crop_out_of_boundsCrop rectangle leaves the framex+width <= 1 and y+height <= 1
invalid_filtergraphEmpty or too long filtergraphAt most 2048 characters, filters only
ffmpeg_fields_rejectedFields like codec or crfNot 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

All Developers posts

Written by Sume