A job id is not a run id: poll image and video generation via /v1/jobs
Image and video generate routes create jobs, not runs. Poll GET /v1/jobs/:id/status until terminal is true. waitForRun is for run ids only.

Runs and jobs are different surfaces with different ids, and you cannot swap them. POST /v1/image-1.0/generate, POST /v1/video-1.0/generate, the Avatar routes and the /v1/models/sume/…/runs aliases all create generation **jobs**, which live at /v1/jobs/:id. waitForRun and the /v1/format-runs/… routes are for Format, Action and Agent runs. Pass a job id to a run helper, or a run id to a job route, and the id will not resolve: the docs say you cannot use a job id as a run id or a run id as a job id.
Two surfaces, side by side
| Runs | Generation jobs | |
|---|---|---|
| Created by | Format, Action or Agent Completion calls | Image, video and Avatar generation routes |
| Read at | /v1/format-runs/…, /v1/action-runs/…, /v1/agent-runs/… | /v1/jobs/:id, /status, /result, /events |
| Wait with | waitForRun with a required family | waitForJob, or your own poll of /jobs/:id/status |
| Done when | Terminal run status | terminal: true on the status route |
| Webhook events | *.run.terminal | job.* events such as job.completed |
The SDK docs describe waitForJob for the job surface. If the version you installed does not export it, poll the status route yourself; the loop is short, and the sample below does exactly that. Check what your installed package exports before you build on a helper.
A job poll that follows the docs
The recommended client behavior for jobs is: treat queued and processing as normal, use exponential backoff, and poll until terminal: true or your own application deadline. The status route also returns next_poll_after_seconds, which the sample uses as the next delay. On 429 or a 5xx it waits retry-after, since a failed read does not mean the job failed.
const key = process.env.SUME_API_KEY;
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
if (!key) throw new Error("set SUME_API_KEY");
const get = (path) => fetch(`${base}${path}`, { headers: { "x-api-key": key } });
// Generation jobs live at /v1/jobs/:id. A job id is not a run id.
export async function pollJob(jobId, deadlineMs = 15 * 60_000) {
const end = Date.now() + deadlineMs;
let wait = 3;
while (Date.now() < end) {
const res = await get(`/jobs/${jobId}/status`);
if (res.status === 429 || res.status >= 500) {
wait = Number(res.headers.get("retry-after")) || Math.min(wait * 2, 60);
} else if (!res.ok) {
throw new Error(`status ${res.status}`);
} else {
const raw = await res.json();
const s = raw.data ?? raw;
if (s.terminal) return (await get(`/jobs/${jobId}/result`)).json();
wait = s.next_poll_after_seconds ?? Math.min(wait * 2, 60);
}
await new Promise((r) => setTimeout(r, wait * 1000));
}
throw new Error("your deadline passed; the job may still be running");
}
console.log(await pollJob(process.argv[2] ?? "job_123"));Rules that apply to jobs
- Do not submit a paid request again only because your local worker timed out. The job may still be running and billing.
- Use an
Idempotency-Keyon each paid submit that a client can retry. - Store
status_url,result_url,events_urlandcancel_urlwhen they are present on the response. - Read
generation_limitson the response. When queue capacity is low, do not add more work. - Job reads are creator-only: an API key reads only the jobs its own member created, and any other read gives
404 not_found. - A job can be canceled only before generation starts. Later you get
409 job_generation_already_started.
Picking the right wait for your id
If you are unsure which surface an id came from, look at where it was created. An id you got from a format-runs URL, an action-runs URL or an agent-runs URL is a run, and waitForRun needs its family because the helper cannot infer it from the id. An id you got back from a generation route, or that you see under /v1/jobs, is a job. Store the surface next to the id when you create it, and your recovery code never has to guess.
A run can produce several jobs inside one agent turn, and the run webhook fires once for the turn. If you want one event per job, subscribe to the job.* events on the job layer instead.
From the CLI
The CLI follows the same model: sume jobs status job_123 --agent --json for the poll, sume jobs result when it is terminal, and sume jobs events for the audit trail. A job you lost track of can be recovered with sume jobs list and sume jobs get. See the SDK runs page for waitForRun and its family option, and keep the rule in mind: the three run families sit behind three URL prefixes, and a job id belongs to none of them.
Sources
Related posts
More in Developers
- Job id or run id? Which Sume endpoint to poll for each product
Jobs, Format runs, Actions and Agent Completions have different ids, poll URLs and webhook events. Which to poll for each product, and which SDK helper to call.
- sume jobs cancel needs --confirm-submit, and only queued jobs cancel
Sume CLI cancel is a write: sume jobs cancel <job_id> --confirm-submit. It works only before generation starts; later: 409 job_generation_already_started.
- GET /v1/jobs/:id returns 404 for a job your teammate's key created
A Sume job id can answer 404 not_found to your key though it exists: an API key reads only jobs its own member created, and only that member can cancel.
- jq one-liner: export Sume bulk queue items to CSV by index
Turn the bulk queue receipt from a curl poll into CSV rows of index, status, run_id and error code with jq, then paste them next to your SKU column.
Written by Sume