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.

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.
| Status | Body | What to do |
|---|---|---|
| 200 | data[] with url and media_type, plus usage.cost | Read the URLs |
| 202 | data.job, data.status_url, data.result_url | Poll the status URL with backoff, then fetch the result |
| 400 | error with a code such as invalid_request | Fix 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
datais 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
- ky retry on POST for Sume: Idempotency-Key, 40 s timeout, v2 baseUrl
ky does not retry POST by default and times out at 10 s, but Sume sync can hold 30 s. A tested ky v2 config with a stable Idempotency-Key and no 429 retries.
- Launch-week 503 provider_capacity_exceeded: safe video submit retries
New video models cause capacity spikes. Retry 429 and 503 on Sume with the same Idempotency-Key, honor retry-after, never retry 402. Python sample included.
- Let a browser poll your backend, not the Sume API: a proxy pattern
Keep SUME_API_KEY on the server: submit async, return the job id, and give the browser a read-only status route. A 27-line TypeScript route with the checks.
- Lint a Format package in CI before you PUT it
A short Python check for a Sume Format package: SKILL.md name equals the slug, allowed folders and extensions, file names, 100 MiB limits. Run it before PUT.
Written by Sume