Image API returned 202, not an image: one Python handler for both

POST /v1/images waits 30 seconds, then returns a 202 job envelope. A Python handler that reads the status code, polls the job and returns image URLs either way.

4 min readSume
All posts

Yes, a POST /v1/images call can return 202 with a job envelope instead of the image, and your code must handle both. Branch on the HTTP status code, not on the body: 200 carries data[].url, and 202 carries data.status_url and data.result_url to poll.

The Image API docs say the call blocks for up to 30 seconds and returns the images with 200 if the work finishes in that budget. If it does not finish, you get 202 with the standard job envelope. Slow settings, such as 4K, high quality and a large n, are the most likely to land on 202.

The two response shapes

The two bodies share no fields, so a client that reads data[0].url straight away will fail on the 202 path. The shapes below are from the docs.

Image API response by status code (Image API docs, read 2026-10-10)
StatusBodyWhat to do
200data[] with url and media_type, plus usage.costRead the URLs
202data.job, data.status_url, data.result_urlPoll the status URL with backoff, then fetch the result
400error with a code such as invalid_requestFix the request, do not retry as is

A handler that returns URLs in both cases

The sample below posts a request and returns a list of URLs. On 202 it polls status_url until the job reaches a terminal status, then reads result_url. The job result uses the standard Sume job shape described in Jobs and results, not the image body, so print the result once on your own account and map the field that holds the media URLs before you rely on it.

It uses httpx and runs under asyncio.run. The model and prompt are placeholders; use a model from GET /v1/images/models.

import asyncio, os
import httpx

BASE = "https://api.sume.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}


async def generate(client, body):
    r = await client.post(f"{BASE}/v1/images", json=body, headers=HEADERS)
    r.raise_for_status()
    if r.status_code == 200:
        return [item["url"] for item in r.json()["data"]]
    env, delay = r.json()["data"], 2
    while True:
        s = await client.get(env["status_url"], headers=HEADERS)
        s.raise_for_status()
        status = (s.json().get("data") or s.json()).get("status")
        if status in ("completed", "failed", "canceled"):
            break
        await asyncio.sleep(delay)
        delay = min(delay * 2, 30)
    return (await client.get(env["result_url"], headers=HEADERS)).json()


async def main():
    body = {"model": "bytedance-seed/seedream-4.5", "prompt": "a mug on linen"}
    async with httpx.AsyncClient(timeout=60) as client:
        print(await generate(client, body))


asyncio.run(main())

What not to do on the 202 path

Do not send the original paid request again because your own client timed out. The job is already running, and a second call would create a second job. The Jobs and results page says this directly: poll with exponential backoff, stop on completed, failed or canceled, and do not resubmit.

Do not fetch the result early either. Results are available only after completion, and an unfinished job gets a conflict response, not an empty result. That is why the sample waits for a terminal status first. A client timeout in your HTTP library, for example 30 seconds, will fire at about the same moment the server gives up waiting, so set your timeout a little above 30 seconds or you will misread a healthy 202 as a network error.

Last, treat 200 and 202 as equally normal. A model that finishes in 12 seconds today may take 40 seconds on a busy afternoon, and the same code should serve both.

Ways to avoid the wait entirely

If you already know the work is slow, ask for the job up front. mode: "async" returns the envelope at once, and mode: "webhook" with a webhook_url makes Sume call you on the terminal event; see Webhooks for the delivery contract. wait_timeout_seconds accepts 0 to 30 on this route, so you can also shorten the wait.

One more trap: mode: "subscribe" is an alias of sync on this route. It gives one bounded 30-second wait, not a progress stream, so it will not spare you the 202 branch.

  • Branch on status code, never on whether data is an array.
  • Failed and cancelled generations are not billed, so a poll loop that gives up does not leave a charge.
  • Set a poll ceiling in your code so a stuck job does not hold a worker forever.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume