One TypeScript job shape for Sume /v1/videos and /v1/jobs responses

Sume's /v1/videos returns a bare object and /v1/jobs wraps in data. A short normalizer maps both to one state, including the cancelled versus canceled spelling.

6 min readSume
All posts

Write one normalize(payload) function that detects which route produced the payload. A /v1/jobs/{id}/status response nests under data and carries sume_status; a /v1/videos/{id} response is a bare object with status and polling_url. Map both to a single {id, state, terminal} and the rest of your code never branches on route.

The two vocabularies

The two routes spell states differently. Note cancelled with two Ls on the videos route versus canceled on the jobs route; a typo there leaves cancelled jobs looking non-terminal forever.

Status values by route (read 2026-10-04)
Our state`/v1/jobs` sume_status`/v1/videos` status
queuedqueuedpending
runningprocessingin_progress
donecompletedcompleted
failedfailedfailed
canceledcanceledcancelled
expired(not used)expired

Normalizer

The function throws on a status it does not know, because a silent default would turn a new state into an infinite poll. Run it with node --experimental-strip-types or compile it first.

type State = "queued" | "running" | "done" | "failed" | "canceled" | "expired";
export type Job = { id: string; state: State; terminal: boolean };

const JOBS: Record<string, State> = { queued: "queued", processing: "running", completed: "done", failed: "failed", canceled: "canceled" };
const VIDEOS: Record<string, State> = { pending: "queued", in_progress: "running", completed: "done", failed: "failed", cancelled: "canceled", expired: "expired" };
const OPEN = new Set<State>(["queued", "running"]);

export function normalize(p: any): Job {
  let id: string, raw: string, table: Record<string, State>;
  if (p?.data?.sume_status) { id = p.data.request_id; raw = p.data.sume_status; table = JOBS; }
  else if (typeof p?.status === "string" && p?.polling_url) { id = p.id; raw = p.status; table = VIDEOS; }
  else throw new Error("unrecognised job payload");
  const state = table[raw];
  if (!state) throw new Error(`unknown status ${raw}`);
  return { id, state, terminal: !OPEN.has(state) };
}

console.log(normalize({ data: { request_id: "job_1", sume_status: "processing" } }));
console.log(normalize({ id: "vid_1", polling_url: "https://api.sume.com/v1/videos/vid_1", status: "cancelled" }));

Where this breaks

Read the response envelope too. Jobs wrap in data; the videos route does not. The videos route can also report expired, which the jobs route does not use. Where both are present, prefer the jobs route's own terminal flag over the derived one.

Related

If you are porting a call from OpenAI's Sora Videos API, see keeping a Sora-style call and mapping it onto Sume.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume