Two failure channels in the Sume SDK: submit error vs failed job

A generateVideoV1 error means no job exists; a failed job means one did and billing was settled. Handle both channels in TypeScript without double-submitting.

4 min readSume
All posts

A Sume video call can fail in two different places, and they need different code. If generateVideoV1 returns an error, the submit was refused and there is no job to wait for. If it returns data and waitForJob later resolves with a failed status, a job existed and ran; its failure is in the job record. Mixing the two leads to either lost jobs or paid duplicates.

The SDK uses the { data, error, response } result shape, so the first channel is a value you check, not an exception. The second channel is a status you read.

The two channels side by side

Table rows combine the jobs docs and the SDK page. A client-side timeout is a third case: it throws and the job keeps running.

Where each failure shows up (read 2026-10-07)
CaseWhere you see itJob exists?Right next step
401, 400, 402, 404 on submiterror from generateVideoV1NoFix the request or the account; do not loop
429 rate_limited or queue_fullerror from generateVideoV1NoBack off, retry with the same Idempotency-Key
Terminal failedstatus and error on the job recordYesRead the error category and next_action
Terminal canceledstatus on the job recordYesNothing to fetch; /result returns 409
SumeJobTimeoutErrorThrown by waitForJobYes, still runningKeep the job id; resume or cancel

Why the split matters

A refused submit has not reserved anything. A job that failed after admission is settled through the usage ledger, which reserves credits when work is accepted and captures or refunds them at the end. So the failed-job path should read the record and decide about a fresh submit with a new key, while the refused-submit path should repeat the same call with the same key.

GET /v1/jobs/{id}/result is not the place to read a failure: a failed or canceled job answers 409 job_not_completed there. Read the job record instead.

One function for both

The function below returns a small result object so callers never see raw SDK shapes. It stores nothing; in your app, write the job id to your database right after the submit.

import { createSumeClient, generateVideoV1, waitForJob } from "@sume-com/sdk";

const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });

export async function makeClip(prompt: string, key: string) {
  const { data, error } = await generateVideoV1({
    client,
    headers: { "idempotency-key": key },
    body: { prompt, mode: "async" },
  });
  if (error || !data) return { kind: "refused" as const, error };

  const jobId = data.data.request_id;
  try {
    const job = await waitForJob(jobId, { client });
    if (job.status === "completed") return { kind: "done" as const, jobId, job };
    return { kind: "job_ended" as const, jobId, job };
  } catch (err) {
    return { kind: "still_running" as const, jobId, err };
  }
}

What to show users

Map the four kinds to four messages: "could not start", "finished", "did not finish, try again", and "still working". Never show raw provider text; Sume keeps public errors provider-neutral, and gives a request id you can quote to support. The webhook path has the same split: job.failed and job.canceled are terminal events, not delivery errors.

Logging that survives an incident

Log one line per channel with the same fields: key, job id if any, kind, and the request id from the error body. When a customer says "my video vanished", the line tells you whether a job ever existed.

Keep the idempotency key in your own order row. If your process dies between the submit and the database write, the same key on a restart returns the original job instead of creating a second paid one.

  • Refused: log the status and error.code.
  • Job ended: log status, the error category and next_action.
  • Still running: log the job id and the deadline you gave up at.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume