FastAPI background poll of a Sume job: use next_poll_after_seconds
Poll a Sume job from an asyncio task in a FastAPI 0.142 app, wait for the server-provided interval, stop at terminal, and never resubmit after a timeout.

Poll a Sume job from FastAPI with a plain asyncio coroutine that sleeps for next_poll_after_seconds between reads and stops when terminal is true. Keep the webhook as the main path and use this loop as the fallback for a delivery that never arrives. FastAPI 0.142 added OpenTelemetry in the same week, so a loop like this is also a good place to attach a job id to a span.
What the job record tells you
The job envelope has status (queued, processing, completed, failed, canceled), a boolean terminal, result_ready, and next_poll_after_seconds. Wait at least that long. The SDK helper for the same job type, waitForJob, treats its 2 second interval as a floor and lets the server value win when it is longer.
| Field | Use |
|---|---|
terminal | Stop polling when true |
result_ready | Fetch the result only when true |
next_poll_after_seconds | Sleep this long before the next read |
status | failed and canceled are terminal without a result |
A loop that does not resubmit
The function below reads GET /v1/jobs/{id} with the x-api-key header and returns the last record. It uses only the standard library, run in a thread so the event loop stays free. A timeout returns the last record; it does not cancel the job, and it does not submit a second one.
import asyncio, json, os, time, urllib.request
def read(job_id: str) -> dict:
req = urllib.request.Request(
f"https://api.sume.com/v1/jobs/{job_id}",
headers={"x-api-key": os.environ["SUME_API_KEY"]},
)
with urllib.request.urlopen(req, timeout=15) as r:
body = json.load(r)
return body.get("data", body)
async def wait(job_id: str, deadline_s: float = 600) -> dict:
end = time.monotonic() + deadline_s
while True:
job = await asyncio.to_thread(read, job_id)
if job.get("terminal") or time.monotonic() > end:
return job
await asyncio.sleep(max(2, job.get("next_poll_after_seconds") or 2))
async def main() -> None:
print(await wait("job_REPLACE_ME"))
if __name__ == "__main__":
asyncio.run(main())Where to run it in FastAPI
Start the loop with asyncio.create_task from a route, and keep a reference to the task so it is not garbage collected. For anything longer than a request, store the job id in your database and let a worker poll.
- A job is readable only by the member whose key created it, so poll with the same key.
- On
429 rate_limited, readretry-afterand back off. - Fetch
GET /v1/jobs/{id}/resultonly whenresult_readyis true; before that it answers409 job_not_completed.
Why the interval comes from the server
A fixed 1 second loop wastes status reads, and status reads have their own rate limits. The hint lets Sume slow every client down at once when the queue is long.
Failure cases to handle
Three outcomes need different handling. A failed job carries an error and no result, so show the error and stop. A canceled job is terminal with no result. A deadline that expires in your loop is not a Sume state at all: the job is still queued or processing, so store the id and look again later.
If the read itself fails with 401, check that the same key that created the job is the one you send. A job is readable only by the member whose key created it, which is a common surprise when a worker uses a different key from the web app.
If you poll more than a handful of jobs, move to mode: "webhook". Polling costs status reads that count against rate limits, and a webhook costs one request per job. Keep this loop as the fallback for the case where a delivery never arrives, because Sume gives up after 10 attempts.
Sources
Related posts
More in Developers
- Fastify: verify a Sume video webhook where Sora's video.completed was
Swap the Sora video.completed handler for Sume's job.completed in Fastify. Keep the raw string body, verify sume-v1 with the SDK, and refuse an empty secret.
- Find the timestamp of a quote in a recording with Sume STT words
Sume's STT result returns every word with start and end seconds. A short Python function finds a quoted phrase and returns where to cut. About a cent a minute.
- First-frame image for /v1/videos: public HTTPS only, no signed URLs
Image and video inputs to Sume generation must be fetchable public HTTPS URLs. Localhost, private IPs, signed URLs and wrong content types are rejected.
- Free, Pro, Startup, Scale: processing seats, queue slots, full hold
Sume's concurrency by plan, queue capacity max(3, 5 x concurrency), accepted job capacity, and the balance reserved if every slot holds a 10 s clip.
Written by Sume