Sume Images API has no b64_json: turn data[].url into base64
POST /v1/images returns a Sume-hosted data[].url, not b64_json. Fetch the URL and encode it yourself; 202 means poll the job. Python example inside.

Sume does not return b64_json from POST /v1/images. Each image comes back as data[].url, a Sume-hosted signed URL, plus media_type. If your code expects base64 inline, download the URL and encode the bytes yourself (example below).
The reason is stated in the Image API docs: Sume already mirrors generated media to storage, so adding base64 to the response would double the bytes. The same page says the API returns data[].url in its place.
What the response contains
The sync response is the OpenRouter-shaped body. Read these fields and ignore the ones you used to read from a base64 client.
| Field | What Sume returns |
|---|---|
| data[].url | Sume-hosted, signed URL of the generated image |
| data[].media_type | For example image/png or image/webp, following output_format |
| data[].b64_json | Not returned |
| usage.cost | The billed USD amount for the call |
| usage.prompt_tokens, completion_tokens, total_tokens | Always 0 in v1; Sume meters image models per image |
| created, model | Unix time, and the model id you requested (sume/auto stays sume/auto) |
Fetch the URL and encode it
The sample sends one low-quality request, handles the two documented status codes, then encodes the bytes. A 200 carries the image body. A 202 carries the job envelope, because the wait budget (30 seconds on this route) ran out or you asked for async.
Set SUME_API_KEY first. On a 202, poll status_url until terminal is true and then read result_url, as described in Jobs and results.
import base64
import os
import requests
resp = requests.post(
"https://api.sume.com/v1/images",
headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
json={"model": "openai/gpt-image-2.5", "prompt": "a red kettle on a white table", "quality": "low"},
timeout=60,
)
if resp.status_code != 200:
raise SystemExit(f"not a 200 ({resp.status_code}): poll the job. {resp.text[:200]}")
body = resp.json()
url = body["data"][0]["url"]
b64 = base64.b64encode(requests.get(url, timeout=60).content).decode()
print(body["data"][0]["media_type"], len(b64), "base64 chars, cost", body["usage"]["cost"])Gotchas when you port a base64 client
- Branch on the status code, not the body shape:
200is the image response,202is the job envelope. Slow settings (4K, high quality, largen) are the likeliest to fall to202. - Download soon and store the bytes in your own bucket if you need them long term; the URL is signed.
- Do not read
usage.total_tokensfor cost. It is0;usage.costis the number to log. output_formatdecides the media type. Sendpng,jpegorwebpwhere the catalog row lists it.
A reusable helper
If several services in your stack expect base64, put the download in one function and keep the rest of the code unchanged. Return the bytes and the media type together, so a caller can build a data URI without guessing the format. Set a timeout on the download, check the HTTP status, and fail loudly if the URL has expired.
Two cautions apply. First, base64 inflates the payload by about a third, which is part of why Sume returns a URL: if you forward the string to a browser or a queue, you move more bytes than the file has. Second, do not log the string; log the job id and usage.cost and keep the URL as the reference. When the result is just an input to another Sume call, such as a reference image for a later edit, pass the URL itself.
Where to read the rules
The Image API page lists the response format, the long-running behavior, and the rule that a parameter a model does not list returns 400 unsupported_parameter. For handing the image to another step, such as a video first frame, pass the URL, not base64.
Sources
Related posts
More in Developers
- No progress percent in Sume job events: what to log and show instead
Sume's job events are a public timeline of eight named events, not a progress feed. What each means, what to log, and what to show a user while a video renders.
- A Sume job looks stuck: wait, cancel or poll the events?
Read status, then events. Queued and processing mean wait, cancel only works before generation starts, and a client timeout never cancels the job.
- Sume job webhooks: 10 attempts, 30 seconds apart, at least 270 s
Ten attempts with a fixed 30-second gap cover at least 270 seconds. What that means for your deploys, cold starts and when to fall back to polling a Sume job.
- sume login --no-browser on a remote server: approve the user_code URL
On an SSH box, sume login --no-browser prints the approval URL instead of opening a browser. After you approve the user_code, the CLI stores a CLI-scoped key.
Written by Sume