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.

Poll the resource that the submit call created. A model or product endpoint such as POST /v1/image-1.0/generate or /v1/video-1.0/generate creates a generation job, and you poll /v1/jobs/{id}/status. A Format, an Action or an Agent Completion creates a run, and you poll /v1/format-runs/{id}/status, /v1/action-runs/{id}/status or /v1/agent-runs/{id}/status. A job id is not a run id, and a run id is not a job id.
The four resources side by side
Sume documents jobs and runs as separate surfaces with separate vocabularies. Runs wrap a whole agent turn that may itself create several jobs; a job is one unit of generation work. The poll and result patterns look alike on purpose, but the paths, the error codes and the webhook events are different.
| Product | Created by | Poll | Result while running | Webhook events |
|---|---|---|---|---|
| Generation job | Model and product submits such as /v1/image-1.0/generate | GET /v1/jobs/{id}/status | 409 job_not_completed | job.completed, job.failed, job.canceled |
| Format run | POST /v1/formats/{handle}/{slug}/runs | GET /v1/format-runs/{id}/status | 409 run_not_completed | format.run.terminal (completed or failed) |
| Action run | POST /v1/actions/{id}/runs | GET /v1/action-runs/{id}/status | 409 run_not_completed | action.run.terminal (completed or failed) |
| Agent Completion | POST /v1/agent/completions | GET /v1/agent-runs/{id}/status | 409 run_not_completed | agent.run.terminal (completed or failed) |
What differs beyond the path
- Statuses. Jobs run
queued,processing, thencompleted,failedorcanceled. Format runs addskipped, for a run that never started becauseon_active_run: "skip"found another run in progress. - Events. Jobs and Format runs have an events route (
/v1/jobs/{id}/events,/v1/format-runs/{id}/events). Action runs do not;events_urlis alwaysnullon their receipts. The Format route is a phase timeline, not a log stream. - Webhook payload. A job webhook carries
job_idand a result payload. A run webhook carries the full run receipt underpayload, plusoutcomeofok,degradedorerror. - Cancel. Both cancel routes are idempotent, but a job can only be canceled before generation starts, and a canceled run sends no webhook at all, so poll the status after you cancel.
- Scopes. Runs need
formats:*,actions:*oragent_completions:*scopes on the key. Jobs are read by the key that created them.
One helper per resource
In TypeScript, waitForJob waits on a generation job and waitForRun waits on a run. waitForRun needs a family of format, action or agent because a run id does not say which of the three URL prefixes it lives under. The sketch below routes one id to the right helper. Both resolve with the terminal record, and neither throws just because the outcome was a failure, so read status and error afterwards.
import { createSumeClient, waitForJob, waitForRun } from "@sume-com/sdk";
const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });
type Kind = "job" | "format" | "action" | "agent";
export async function finish(kind: Kind, id: string) {
// Generation jobs (job_...) and runs (a different id) are different resources.
if (kind === "job") return waitForJob(id, { client });
return waitForRun(id, { client, family: kind });
}Reading the receipt of a run
A run receipt is richer than a job record because it describes an agent turn. It holds status, output, artifacts[], primary_output_url, usage, and the URLs status_url, result_url and cancel_url. A poll response wraps the receipt in data, while a webhook delivers the same object unwrapped under payload, so one handler can serve both transports. The small status_url payload is built for polling: it carries status, next_action, cancelable, expires_at and timestamps, but never the output or the artifacts.
next_action tells your client what to do next without a status table. On a Format run it takes only three values: poll_status while the run is queued or processing, retry_later on a skipped run, and none on every terminal run. expires_at is the deadline after which Sume force-finalizes a stuck run as failed, so use it as your own timeout ceiling instead of inventing one. A job status payload carries terminal, result_ready and, when present, next_poll_after_seconds.
The mixing mistakes to avoid
The costliest mistake is asking the wrong prefix. A run id sent to /v1/jobs/... or a job id sent to /v1/format-runs/... is a different resource, so you will not get the record you want. Store the kind of resource next to the id when you save it, not just the id.
The second mistake is treating a Format run's webhook like a job webhook. They share one signature scheme, so one verifier covers both, but you should route on event and never assume the body has run_id or job_id. Return a plain 2xx for an unknown event name so that a new event type does not cause a retry storm.
Sources
Related posts
More in Developers
- Let browsers start Sume jobs through your server, not with your key
Browsers must never hold a Sume API key. A server route authenticates the user, checks the input, derives an Idempotency-Key and returns only the status URL.
- How do I add a listen-to-this-page audio version with TTS?
Turn each article into an audio file with one async TTS job per page: a 9,000-character article costs 43 cents on Sume. What it does not replace.
- Mandarin Chinese speech to text API: Sume STT language_code zh
Transcribe Mandarin audio with Sume STT using language_code zh, then check the result and timings. $0.01 per audio minute and no accuracy claim without a test.
- Migrate a real-time avatar prototype to Sume async jobs: what changes
Moving from a live avatar session to Sume means replacing a stream with submit, poll and fetch. The code changes, the UX changes, and a Node example that runs.
Written by Sume