Sume 400 unsupported_capability on 4:5: fall back to 3:4 in Python
A 4:5 request on a Sume video model returns 400 unsupported_capability before billing. Catch it in Python, resubmit at 3:4, then crop to Meta's 4:5 Feed ratio.

If you send aspect_ratio 4:5 to a Sume video model that does not advertise it, the API answers 400 with code unsupported_capability and creates no job. Catch that error, resubmit at 3:4, and crop the result to the 4:5 that Meta lists for Feed.
What the docs promise
Sume's video contract says the catalog controls validation. If a model does not advertise a resolution, aspect_ratio, duration, frame_type or reference type, the value gives a 400 and the API does not silently drop the field. The error table lists 400 unsupported_capability for a value that is not in the resolved model's advertised support list, and it says error bodies keep the public error envelope, with error.code and error.message.
A 400 happens at validation, before submission to the provider. Billing reserves on a successful submit, so a rejected request is not charged.
| HTTP | code | Meaning | Retry? |
|---|---|---|---|
| 400 | unsupported_capability | Value not advertised by the model | Change the value |
| 400 | unsupported_parameter | size, seed or provider.options sent | Remove the field |
| 402 | insufficient_credits | Balance below the reserve | Top up |
| 409 | conflict | Idempotency-Key reused with a different body | New key |
The fallback in Python
The script tries 4:5 first with its own idempotency key, reads the error body when the call is refused, and falls back only for unsupported_capability. Any other error is re-raised so you do not mask a real problem. Different bodies need different keys, otherwise the second call would hit the 409 conflict row.
import json, os, urllib.error, urllib.request
def submit(ratio):
body = json.dumps({
"model": "seedance-2",
"prompt": "A leather wallet on a desk, soft light",
"duration": 5, "resolution": "720p",
"aspect_ratio": ratio,
}).encode()
req = urllib.request.Request(
"https://api.sume.com/v1/videos", data=body,
headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "wallet-" + ratio.replace(":", "x")})
with urllib.request.urlopen(req) as r:
return json.load(r)
try:
job = submit("4:5")
except urllib.error.HTTPError as e:
code = json.load(e).get("error", {}).get("code")
if code != "unsupported_capability":
raise
print("4:5 refused, using 3:4")
job = submit("3:4")
print(job["id"], job["status"])After the fallback
Poll the job, then crop with video-filter: x 0, y 0.03125, width 1, height 0.9375, which turns 3:4 into 4:5. Run the free check endpoint first. The crop post shows the numbers.
If the model you pinned does advertise 4:5 someday, the first call succeeds and the fallback never runs, so the code stays correct as the catalog changes.
Why not use sume/auto
sume/auto resolves to a model whose envelope is 16:9 or 9:16 with 3 to 10 seconds and native audio, so a 4:5 request there also returns 400. The message names sume/auto, not the resolved family, because the family stays opaque.
Sources
Related posts
More in Developers
- Sume 429: read error.details.scope, and rate_limit_unavailable
A Sume 429 names the budget in error.details.scope and gives retry_after_seconds. A degraded rate_limit_unavailable 429 is a different case. Python handler.
- Sume schedule run 403: a service-account key cannot start action runs
Starting a Sume schedule run with a service-account key fails with 403 insufficient_scope and service_account_action_runs_unsupported. Use a user API key.
- Why sume/auto returns 400 for 21:9, not a Seedance route
sume/auto accepts 3 to 10 s in 16:9 or 9:16. Ask for 21:9 or 15 s and you get 400 unsupported_capability, not a quiet reroute to Seedance.
- sume/auto vs a pinned video model: three jobs where each wins
sume/auto is right for an 8-second 720p clip at $1.00. A 12-second or 20-second clip needs a pinned id, because auto resolves to Omni with a 10-second cap.
Written by Sume