Image API returned 202 after 30 seconds: a Python poll that finishes

POST /v1/images waits 30 seconds, then returns 202 with a job envelope for slow high-quality runs. A 25-line Python script that handles both 200 and 202.

5 min readSume
All posts

POST /v1/images blocks for up to 30 seconds. If the image is ready, you get 200 with data[].url; if not, you get 202 with a job envelope and must poll the status URL, then fetch the result. The Sume docs say slow configurations, meaning 4K, high quality and large n, are the most likely to degrade to 202, so a high-quality GPT Image 2.5 call is a good candidate.

Branch on the status code, not the body

The Image API docs are explicit: examine the status code, not the body shape. 200 is the image response. 202 is the standard job envelope with data.status_url and data.result_url. The result you fetch afterwards uses the standard job result shape, not the image body, so read the job docs for that shape rather than assuming the 200 layout.

What each response means (from docs.sume.com, read 2026-10-08)
StatusBodyNext step
200Image response with data[].url and usage.costDownload the URLs
202Job envelope with status_url and result_urlPoll status until terminal, then GET result
Other 4xx/5xxError bodyFix the request; failed generations are not billed

The script

It posts once, returns on 200, and on 202 polls the status URL honoring next_poll_after_seconds when present. It stops on terminal and fetches the result when result_ready is true. The overall deadline lives in your client, since a client-side timeout does not cancel the job.

import json, os, time, urllib.request

KEY = os.environ["SUME_API_KEY"]
H = {"Authorization": "Bearer " + KEY, "Content-Type": "application/json"}

def call(url, body=None):
    req = urllib.request.Request(url, json.dumps(body).encode() if body else None, H)
    with urllib.request.urlopen(req, timeout=40) as r:
        return r.status, json.load(r)

body = {"model": "openai/gpt-image-2.5", "prompt": "studio shot of a red kettle",
        "quality": "high", "aspect_ratio": "4:3"}
code, out = call("https://api.sume.com/v1/images", body)
if code == 202:
    env = out["data"]
    deadline = time.time() + 600
    while time.time() < deadline:
        _, s = call(env["status_url"])
        if s.get("terminal"):
            break
        time.sleep(s.get("next_poll_after_seconds") or 3)
    _, out = call(env["result_url"])
print(json.dumps(out)[:400])

Things that trip people up

These are the mistakes that show up most when a client first meets a 202.

  • Do not read data[].url from a 202 body; there is none yet.
  • GET /v1/jobs/:id/result answers 409 job_not_completed for jobs that did not complete, so read the failure from the job record (GET /v1/jobs/:id).
  • A client timeout stops your wait, not the job, and the job still bills if it completes.
  • Prefer mode: "async" or mode: "webhook" with a webhook_url if you know the call will be slow; the 202 envelope comes straight back.

Choosing sync, async or webhook up front

The 30-second sync wait is a convenience, not a contract. For an interactive tool where a person is watching, sync with a spinner is fine for low and medium quality: the images usually come back inside the budget. For a nightly batch of 200 high-quality renders, skip the guesswork and submit every call with mode: "async", store the job ids, and let a worker poll them.

Webhook mode is the third option: send mode: "webhook" with a public HTTPS webhook_url and Sume posts a signed terminal event when the job finishes. The job webhook events are job.completed, job.failed and job.canceled, and there are no progress events. If your handler verifies the signature, make it refuse an empty secret rather than treating a missing secret as a pass.

mode: "subscribe" is not a progress stream on this route. The docs describe it as an alias of sync: one bounded 30-second wait. Do not build a progress bar on it.

Idempotency and retries

Send an Idempotency-Key header on submits you may retry, so a dropped connection does not create a second paid job. The job docs show the header on the async submit example. If you retry without a key after an ambiguous failure, you can end up with two completed images and two charges for one prompt.

Keep the key stable per logical request, such as a hash of the prompt, model and parameters plus a run label, and change it when you actually want a new image.

Cost while you wait

A completed GPT Image 2.5 image at high quality is billed at about 7 cents at the catalog's rounding; a call that fails is not billed. Check usage.cost on a 200, and the job record for a 202, to log what each call cost. Webhooks are described in the webhook docs if you would rather be told than poll.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume