Sume job status: stop on terminal, fetch on result_ready
A Sume status reply has two booleans. terminal says stop polling; result_ready says GET /result will work. Failed and canceled jobs stop but have no result.

Short answer
Two booleans on the Sume job envelope answer two different questions. terminal tells you to stop polling, and it is true for completed, failed and canceled. result_ready tells you that GET /v1/jobs/:id/result will return the result, which only a completed job has. Poll until terminal, then fetch only when result_ready is true.
The statuses
A job moves through queued and processing to one of three terminal states. The jobs docs list the full set, and the poller should treat the last three as the end of the line.
Do not treat queued as failure. When a workspace is at its concurrency limit, jobs wait in the queue until a slot frees, and polling with backoff is the correct response.
| sume_status | terminal | Next step |
|---|---|---|
| queued | false | Keep polling |
| processing | false | Keep polling |
| completed | true | GET /v1/jobs/:id/result when result_ready is true |
| failed | true | Read the error from GET /v1/jobs/:id; there is no result |
| canceled | true | Stop; there is no result |
Why not just check for completed
You can; the docs say to fetch the result when a job reports result_ready true or status completed. The booleans are friendlier to code. A loop that tests terminal has no list of status strings to maintain, and a new non-terminal state would not break it. A fetch guarded by result_ready cannot hit the 409 that /result returns for jobs that are not completed.
That 409 is job_not_completed. If you see it, your loop fetched too early or the job ended in failure. Read the job record instead and look at its error, rather than retrying the result call.
id=$1
while true; do
s=$(curl -fsS "https://api.sume.com/v1/jobs/$id/status" \
-H "x-api-key: $SUME_API_KEY") || exit 1
[ "$(echo "$s" | jq -r .terminal)" = true ] && break
sleep "$(echo "$s" | jq -r '.next_poll_after_seconds // 3')"
done
[ "$(echo "$s" | jq -r .result_ready)" = true ] &&
curl -fsS "https://api.sume.com/v1/jobs/$id/result" -H "x-api-key: $SUME_API_KEY"
Pacing the loop
The status reply may carry next_poll_after_seconds. Honor it when present and use your own exponential backoff when it is absent. Reads draw on a separate budget from writes, forty times larger, so a well-behaved loop will not exhaust it, but a tight loop with no sleep wastes requests for no benefit.
A 429 on a read names its scope in error.details.scope and carries retry-after; wait that long. A 404 not_found on a job you know exists usually means the job was created by a different member's key, since job reads through an API key are scoped to the member that owns it.
What a timeout does
If your own polling deadline passes, the job keeps running. Resubmitting creates a second paid job. Store the job id, resume polling later, or cancel; cancel works only before generation starts, and afterward the API answers 409 job_generation_already_started.
Webhooks can replace the loop. Submit with a callback and wait for job.completed, job.failed or job.canceled, keeping a slow poll as a backup. The same two booleans apply when you read the job after the callback arrives.
In tests
Cover three fake status replies: one with terminal false, one completed with result_ready true, and one failed. Assert that the result route is called for the second and never for the third. That single assertion protects you from the most common poller bug, a result fetch on a job that never completed, and it needs no network if you use a mock transport or a fake fetch.
Sources
Related posts
More in Developers
- Music 1.0 is retiring: switch to /v1/music-router/generate in one line
Sume's Music 1.0 routes keep working but now resolve through the Music Router. Which URL to change, what stays the same, and how to see which engine ran.
- Sume ratelimit-limit differs on reads and writes: read the header
ratelimit-limit shows the budget the current call spent from, so a GET shows reads and a POST shows writes. Read the header; do not hard-code the plan table.
- Retry Sume 429s in TypeScript: a fetch wrapper that obeys retry-after
A small fetch wrapper for the Sume API: retry 429 only when the request is a GET or carries an Idempotency-Key, wait retry-after, and never loop on queue_full.
- Sume reserve, capture, refund: what your cost ledger should mirror
Sume reserves the estimate at submit, captures it on success and releases it on failure or cancel. Mirror the three states or cost reports will double count.
Written by Sume