Parse a Sume video poll response with a dataclass, no network
A stdlib Python dataclass for the /v1/videos poll response (status, unsigned_urls, usage.cost, error), tested on sample JSON so a ported client fails early.

A ported video client breaks quietly when it reads a field the new API names differently. Parse the Sume poll response once, into a small dataclass, and the rest of your code touches typed attributes: id, status, model, unsigned_urls and usage_cost. The docs show a completed response with exactly those keys plus generation_id and polling_url (Sume video docs, read 2026-10-06).
The OpenAI Sora API ended on 2026-09-24 (Magic Hour tracker, read 2026-10-06), which leaves many wrappers whose response handling was written for a different shape. A parser plus a sample file is the cheapest test you can add during the move.
The fields that matter
| Field | Type | When present | Use it for |
|---|---|---|---|
| id | string | Always | Your job key in your database |
| status | string | Always | pending, in_progress, completed, failed or cancelled |
| polling_url | string | Always | Where to poll next |
| unsigned_urls | array of strings | On completed | Download the video; index 0 is the first output |
| usage.cost | number | On completed | The Sume billable amount in USD |
| error | string or object | On failed | Decide retry or give up |
The parser
It does not call the network. Feed it the JSON from the docs, from a stored response, or from a live poll.
import json
from dataclasses import dataclass, field
@dataclass(frozen=True)
class VideoJob:
id: str
status: str
model: str
urls: list[str] = field(default_factory=list)
cost: float | None = None
error: object = None
done = property(lambda s: s.status in {"completed", "failed", "cancelled"})
def parse(raw: str) -> VideoJob:
d = json.loads(raw)
for key in ("id", "status"):
if key not in d:
raise ValueError(f"poll response lacks {key}")
cost = (d.get("usage") or {}).get("cost")
return VideoJob(d["id"], d["status"], d.get("model", ""),
d.get("unsigned_urls") or [], cost, d.get("error"))
sample = '{"id":"job_1","status":"completed","model":"seedance-2","unsigned_urls":["https://api.sume.com/v1/videos/job_1/content?index=0"],"usage":{"cost":0.25}}'
j = parse(sample)
assert j.done and j.cost == 0.25 and len(j.urls) == 1
print(j)What it catches
- A missing
idorstatusraises at the edge, not three functions later. - A
completedjob with nounsigned_urlsshows as an empty list, which your caller can treat as an error instead of downloading nothing. coststaysNoneuntil completion, so a budget total never adds a half-known number.- A new status word needs one edit in
done; pair it with the normalizer from the statuses post if you also poll the jobs route.
Downloading
Take urls[0] and fetch it. The docs show the content endpoint also accepts the same bearer key, with an index query parameter that defaults to 0 for models that return more than one output. Save the bytes to your own storage straight away and store your own path next to the job id; do not rely on a URL staying valid for your customers.
Add three more sample files to your test folder: a pending response, a failed one with an error, and a cancelled one. Four small JSON files make a contract test that runs in milliseconds. The earlier posts on a pytest contract test and on unsigned URLs go further on mocking and download behavior.
Fixtures for the contract test
The parser is only as good as the responses you test it with. Keep one fixture file per terminal state in a fixtures/ folder: pending, in progress, completed, failed with an error, and cancelled. Take the completed one from the docs example, and take the others from a real job of your own, with the id and URLs replaced by placeholders.
A test then loads each file, parses it, and checks a few facts: the normalized state, whether unsigned_urls is empty, and whether cost is set. Run it in your normal test suite. It takes milliseconds, needs no key, and fails the day someone edits the parser or the docs add a field you should handle.
If you prefer a validation library to a dataclass, the idea is the same: parse at the edge, fail loudly on a shape you do not know, and give the rest of the program a typed object. The standard library is enough here and adds no dependency to a small worker, which is why the example uses it.
- Add a fixture for an unknown status word and assert that the parser raises.
- Add a fixture where
usageis missing on a completed job, to decide what your code does with a gap. - Keep the real poll responses you log in production for a few days; they are the best source of new fixtures when something surprising shows up.
- Do not put a real signed or authenticated URL into a fixture that lives in a repository.
Sources
Related posts
More in Developers
- Export old video prompts to JSONL, then re-render on Sume resumably
A prompt manifest for a Sora back catalog: one JSON line per clip with a stable idempotency key and status, and a runnable script that skips what is done.
- Photo to video on Sume: frame_images or input_references?
Which Sume video field starts from your photo and which only guides style. If you send both, frame_images wins. Two request bodies and a check you can run.
- Score a Sora replacement: five numbers to log for every clip
Before you cut over from Sora to a Sume video model, log five numbers per clip: status, wall time, usage.cost, duration asked and failures. Script included.
- Prove a Sume video retry is safe: same key, same job, one charge
A runnable test for a ported Sora worker: submit twice with one Idempotency-Key on /v1/videos, assert the same job id came back, and cancel before it bills.
Written by Sume