next_poll_after_seconds is null on a finished job: fix the poll loop
Sume sets next_poll_after_seconds to null once a job is terminal, so sleep(null) crashes naive loops. Stop on terminal, then read result_ready.

On a terminal job, Sume returns next_poll_after_seconds: null, so a loop that always does sleep(status["next_poll_after_seconds"]) throws on the last iteration. Check terminal first, and only sleep when the field is a number.
The same status payload sets result_ready to true only for completed. A failed or canceled job is terminal with result_ready: false, and fetching /result for it answers 409 job_not_completed.
What do the status fields look like at each stage?
In the API's job-status metadata, a non-terminal job reports next_poll_after_seconds (default 2) and recommended_poll_interval_seconds (2), and a terminal job reports null for both. terminal is true for completed, failed, and canceled.
next_action changes with the state too: poll_status while running, fetch_result when completed, inspect_events when failed or canceled.
| Status | terminal | result_ready | next_poll_after_seconds | next_action |
|---|---|---|---|---|
| queued, processing | false | false | number (2 by default) | poll_status |
| completed | true | true | null | fetch_result |
| failed, canceled | true | false | null | inspect_events |
Which field should the loop stop on?
Stop on terminal, not on result_ready. A loop that waits for result_ready runs forever on a failed job, because it never becomes true. The jobs docs recommend polling on the booleans or on sume_status.
After the loop, branch: fetch the result only when result_ready is true, otherwise read the job record for its error.
A poll loop that handles null
Honor the server's hint as a floor when it is present, and fall back to your own backoff otherwise. Put your own deadline outside the loop: a client-side timeout does not cancel the job, which keeps running and billing.
import asyncio, os
import httpx
BASE = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
async def wait(job_id: str, deadline_s: float = 1200) -> dict:
async with httpx.AsyncClient(headers=H, timeout=30) as c:
delay, waited = 2.0, 0.0
while waited < deadline_s:
s = (await c.get(f"{BASE}/v1/jobs/{job_id}/status")).json()
d = s.get("data", s)
if d.get("terminal"):
return d
hint = d.get("next_poll_after_seconds")
delay = max(delay, hint) if hint else min(delay * 1.5, 15)
await asyncio.sleep(delay)
waited += delay
raise TimeoutError(job_id)
async def main():
d = await wait("job_123")
print(d.get("result_ready"))
asyncio.run(main())Is the response wrapped in data?
The submit examples in the API's OpenAPI document wrap the envelope in data. The sample above reads either shape defensively; check the live schema at https://api.sume.com/reference/json for the exact response of the route you call.
Quick checklist
The points above reduce to a short list you can paste into a runbook.
- Check terminal before you read any sleep hint.
- Only sleep when next_poll_after_seconds is a number; otherwise use your own backoff.
- Fetch /result only when result_ready is true.
- For failed or canceled jobs, read the job record or events instead of /result.
- Set a client deadline outside the loop, and remember it does not cancel the job.
Sources
Related posts
More in Developers
- NEXT_PUBLIC_ plus a Sume API key: why it ships to the browser
A NEXT_PUBLIC_ prefix inlines the value into client JavaScript at build time. Keep the Sume API key server-side, proxy via a route handler, rotate if it leaked.
- Poll a Sume job with AbortSignal.any in Node 26.10
Node 26.10.0 fixes AbortSignal.any() propagation. Here is a Sume job poll loop with a hard deadline and a caller cancel, using next_poll_after_seconds.
- Node fetch with AbortSignal.timeout: poll a Sume job
A Node recipe: submit a Sume job with fetch, bound each call with AbortSignal.timeout, poll status_url, read the result. A local timeout does not cancel it.
- Open Graph image 1200x630 with an AI API: generate 16:9, then crop
Meta recommends og:image at 1200 x 630 and a 1.91:1 ratio. Sume has no 1.91:1 option, so generate 16:9 and crop with 12 lines of Pillow. Steps and limits.
Written by Sume