The 202 from POST /v1/videos has four fields: where the rest arrives

The Sume submit response returns only id, polling_url, status and model. Usage, unsigned_urls and error come on the poll. A parser that expects no more.

5 min readSume
All posts

The 202 Accepted body from POST /v1/videos has four fields: id, polling_url, status and model. The status is pending. There is no usage, no unsigned_urls and no error yet; those arrive on the poll at polling_url as the job moves on.

What each field is for

The shape matches the OpenRouter video API, so a client written from those docs reads it without change. polling_url is an absolute URL on api.sume.com, so you can pass it straight to your HTTP client. model echoes what you asked for: a pinned id comes back as that id, and sume/auto comes back as sume/auto.

Fields of the submit response and the poll, from the Sume video docs (read 2026-10-08)
FieldOn the 202On the poll
idyesyes
polling_urlyesyes
statuspendingpending, in_progress, completed, failed or cancelled
modelyesyes
generation_idnoyes (same value as id)
unsigned_urlsnocompleted jobs
usage.costnoonce an amount exists
errornofailed jobs

A parser that expects four fields

Code that reads usage.cost from the 202 will get undefined, and code that assumes unsigned_urls[0] exists right after submit will throw. The parser below takes only what the 202 carries, and it refuses a response without a polling_url, which is the sign that the submit failed.

It also checks the status code, because a 400, 401, 402, 404, 415 or 429 returns the standard error envelope with an error object, not these four fields.

type Submitted = { id: string; pollingUrl: string; model?: string };

export async function submit(body: object, key: string): Promise<Submitted> {
  const res = await fetch("https://api.sume.com/v1/videos", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": key,
    },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (res.status !== 202 || typeof json.polling_url !== "string") {
    const e = json.error ?? {};
    throw new Error(`${res.status} ${e.code ?? "?"} request_id=${e.request_id ?? "?"}`);
  }
  return { id: json.id, pollingUrl: json.polling_url, model: json.model };
}

Why it is short

OpenRouter returns the same thin body on submit, and Sume copies it on purpose: the point of this surface is that a client written from the OpenRouter docs works after you change the base URL and the key. Anything that needs the job to finish, such as a URL or a price, belongs to the poll, where the job has the data.

The base path is https://api.sume.com/v1/videos, with no /api segment, and the model ids are bare catalog ids, which are the two places a ported client usually needs a change.

What to do next

Save id and your idempotency key before you start polling, so a crash between the two does not lose the job. Then poll polling_url about every 30 seconds, as the docs suggest, or wait for a callback_url delivery. The two work together: the webhook is the fast path and the poll is the backup.

An idempotent replay of the submit returns the original job, so calling submit again with the same key after a timeout is safe. Replays and conflicts are covered in the 409 conflict post, and the field list of the finished poll is in the status vocabulary post.

  • The 202 is not a promise of success; the job can still fail.
  • Do not show a price from the 202; read usage.cost from the poll.
  • Do not assume status is queued: that word belongs to /v1/jobs.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume