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.

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.
| Case | Where you see it | Job exists? | Right next step |
|---|---|---|---|
401, 400, 402, 404 on submit | error from generateVideoV1 | No | Fix the request or the account; do not loop |
429 rate_limited or queue_full | error from generateVideoV1 | No | Back off, retry with the same Idempotency-Key |
Terminal failed | status and error on the job record | Yes | Read the error category and next_action |
Terminal canceled | status on the job record | Yes | Nothing to fetch; /result returns 409 |
SumeJobTimeoutError | Thrown by waitForJob | Yes, still running | Keep 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 andnext_action. - Still running: log the job id and the deadline you gave up at.
Sources
Related posts
More in Developers
- generation_spend_cap_usd on a Format run: null is $500, 0 is a 400
On a Format run request, omit generation_spend_cap_usd for the Format cap, send a number up to 500, null for the $500 maximum. 0 or above 500 returns 400.
- Get transcript text from a captioned video: caption jobs return none
A Sume caption job returns the burned video, not the transcript. For text and word times run STT or video inspect with transcribe. Prices and a recipe.
- Go: a context timeout stops waiting on a Sume job, not the job
context.WithTimeout cancels your status read, never the generation. A 29-line Go loop shows the DeadlineExceeded branch, and why the job id must be stored.
- Go client for a Sume bulk queue: transient polls and exit status
Create a Sume bulk queue from Go, poll the status_url with a doubling gap, treat 429 and 503 as transient, and exit 1 on failed items. Standard library only.
Written by Sume