Retry, fix or stop: classify every Sume /v1/videos error in code
One Python function that maps each documented /v1/videos error code (400, 402, 404, 409, 429, 502) to retry, fix or stop, plus the two 409s that look alike.

Classify Sume video errors by the error code in the body, not by the HTTP status. Most are fix-the-request errors, 402 means stop and add funds, 429 and 502 mean retry with the same Idempotency-Key, and the two 409s are opposites: job_not_completed is retryable, job_failed is not. The function below returns one of retry, fix or stop for each documented code.
The documented set
The /v1/videos contract lists a small set of errors. Error bodies use the standard Sume envelope, with error.code, error.message and a request_id that is safe to share with support. Log the request_id on every non-2xx response, and never log the key or signed URLs.
The table is the same data as the code, on purpose. If you keep both in your repo, a code review can compare them line by line, and a new error that Sume adds shows up as an unknown code in your logs instead of being silently treated as a retry.
| HTTP | Code | Reaction | Why |
|---|---|---|---|
| 400 | invalid_request | fix | Missing prompt or malformed body |
| 400 | unsupported_parameter | fix | size, seed or provider.options are rejected loudly |
| 400 | unsupported_capability | fix | Value is outside the model's advertised list |
| 401 | unauthorized | stop | Missing or invalid key |
| 402 | insufficient_credits | stop | Balance is below the reserve |
| 404 | model_not_found | fix | Not a catalog id, alias or sume/auto |
| 404 | job_not_found | stop | Unknown or foreign job |
| 409 | job_not_completed | retry | Content asked for while running |
| 409 | job_failed | stop | Terminal failure, not retryable |
| 429 | rate_limited | retry | Honour retry-after |
| 429 | queue_full | retry | Wait for capacity |
| 502 | provider submission | retry | Same Idempotency-Key |
The function
Use the code, not the status, as the key, with a fallback by status class for codes you have not seen yet. An unknown 5xx is a retry, and an unknown 4xx is a fix, since a client mistake does not heal on its own.
RULES = {
"invalid_request": "fix", "unsupported_parameter": "fix",
"unsupported_capability": "fix", "model_not_found": "fix",
"unauthorized": "stop", "insufficient_credits": "stop",
"job_not_found": "stop", "job_failed": "stop",
"job_not_completed": "retry", "rate_limited": "retry",
"queue_full": "retry", "provider_capacity_exceeded": "retry",
}
def classify(status: int, body: dict) -> str:
code = (body.get("error") or {}).get("code", "")
if code in RULES:
return RULES[code]
if status >= 500 or status == 429:
return "retry"
return "fix" if 400 <= status < 500 else "stop"
samples = [(400, {"error": {"code": "unsupported_capability"}}),
(409, {"error": {"code": "job_not_completed"}}),
(409, {"error": {"code": "job_failed"}}),
(429, {"error": {"code": "queue_full"}}),
(503, {}), (418, {})]
for s, b in samples:
print(s, b.get("error", {}).get("code", "-"), "->", classify(s, b))
The two 409s
Sume deliberately split the content-route 409. When a job is still running, you get job_not_completed with retryable true and a next action of poll status. After a terminal failure you get job_failed with retryable false and a next action of inspect events. The split exists so that clients do not poll forever for a file that will never exist. A 409 also appears when an Idempotency-Key is reused with a different body, and that is a fix, since the key belongs to another request.
Which models trigger which 400
The catalog gates the values, so the same request is valid on one model and a 400 on another. Gemini Omni Flash 1.1 takes 3 to 10 seconds, so duration 30 fails there but passes on Seedance 2.5, which takes 4 to 30. Read supported_durations from GET /v1/videos/models before you submit, and you remove most of the fix-class errors before they cost a round trip. A fix-class error is free, since no provider work started.
Wrap classify in your HTTP helper, not in business code. The helper can then log the request_id, count retries per Idempotency-Key, and cap them at a budget such as five attempts. A retry class without a cap is how a transient error turns into a self-inflicted outage.
- Retry with backoff and jitter. Use retry-after when the response has it.
- Keep the Idempotency-Key on every retry of a submit.
- Stop the whole batch on a 401 or a 402. Every remaining job will fail the same way.
- Read the poll error field for failed jobs. It is the public remap of the worker error, so an unreachable input URL says so.
Sources
Related posts
More in Developers
- Claude Agent SDK init message: check the Sume server status first
Read the init message's mcp_servers statuses before a paid Sume call. failed and needs-auth mean the tools are not usable. pending is not a failure on its own.
- Claude Agent SDK allowedTools mcp__sume__* also allows paid Sume tools
A wildcard in allowedTools approves every tool the Sume server exposes. With an API key that includes paid ones. Name the read tools instead.
- claude -p total_cost_usd vs your Sume bill: which number to trust
claude -p prints total_cost_usd for the Claude side of a run. Sume's generation spend is billed on Sume and read from GET /v1/usage. Here is how to log both.
- Sonnet 5.5 token bill vs Sume spend cap: two meters on one agent run
Sonnet 5.5 tokens bill at the model vendor. Sume generation bills in the Sume wallet. A worked example shows why one cap cannot cover both.
Written by Sume