The implementation problem is not just changing a model string. For Seedream 5.5 on AIHubMix, the workflow is a task lifecycle: submit one request, inspect its status, save the returned image, then send those saved bytes as the reference for a separate edit.

This example requests a blue ceramic-mug product image and then a color-only edit to forest green. It uses doubao-seedream-5-5 with POST /ai/v1/images/generations and flat JSON—not a legacy input envelope or data[0].url response.

Verification boundary: documentation checked on November 12, 2026. Code received local syntax and offline helper checks only; no generation request was executed. Nothing here establishes image quality, editing fidelity, latency, or an actual bill.

Lock down the request contract first

Read the current endpoint schema, selecting the entry by path, not array position. Its model-specific constraints take precedence over a shared parameter list.

Setting Value used here
Model doubao-seedream-5-5
Route POST /ai/v1/images/generations
Body Flat JSON
Size 2048x2048
Image count n = 1
File format png
Response format url

The schema requires model and a nonempty prompt, restricts n to 1, and lists png and jpeg output. The image field accepts a URL, Base64 input, or a {url} object. The images array has a published maximum of 14 references; that limit has not been runtime-tested here. This example uses one reference and sets the default size explicitly, without an outer input object or extra extensions.

Use Python 3.10+ and requests. In Bash, install the dependency and enter the key without putting its value into shell history:

python3 -m pip install requests read -r -s -p 'AIHubMix API key: ' AIHUBMIX_API_KEY printf '\n' export AIHUBMIX_API_KEY 

For deployed code, inject the environment variable through your secret manager. Keep the key out of source files, examples, logs, notebooks, and browser-side code.

Before either creation POST, enable the account's task feature, as required by the async task guide. This prerequisite applies to native /ai/v1 media task endpoints even when waiting synchronously. It is not the same as choosing background execution; only the latter is optional.

Remove legacy assumptions before sending anything

The legacy Doubao adapter documentation lists older IDs under /v1/models/doubao/<id>/predictions with an input envelope. Its inventory does not list 5.5. Use the current discoverable route and flat body; no legacy-generation failure was tested. Copy the hyphenated model ID exactly rather than assuming a dotted spelling is an alias.

Do not carry over top-level quality, background, aspect_ratio, or seed: they are absent from this model's current schema. ByteDance's launch announcement describes native transparent RGBA output, but the gateway schema exposes neither background nor layer_decomposition. PNG alone is not a verified transparency recipe.

Generate once, then save the completed result

The unified image API documentation defines a task response with id, object: image, model, status, output, and error. Do not read it as legacy data[0].url.

Read the short workflow first; the full setup and download helper is in the folded Appendix below. Before executing these examples, load that appendix block once into the same Python session. It defines settings, api_json, and collect without submitting an image request. Then run the short blocks in order. Each POST is a separate paid action; do not rerun it to repair a missing response or failed download.

Submit the image request once

generation = { **settings, "prompt": "Product photograph of one matte blue ceramic mug on a cream tabletop, " "soft side lighting, plain warm-gray background, no text or logos.", } task = api_json("POST", IMAGES + "/generations", generation) 

Inspect the task, then save the PNG

Omitting async means synchronous waiting. HTTP 200 can still contain status: failed. The helper saves the task ID before inspecting status: only completed proceeds to downloading; failed, cancelled, and unknown states stop it. pending and in_progress need existing-task inspection, not another generation.

collect(task, "mug-blue") 

This single call uses the creating key to retrieve the completed output, checks PNG bytes, and saves mug-blue.png. It makes no automatic creation retry and refuses unexpected download hosts or redirects before forwarding the key. The full implementation is in the appendix; this is documentation-based behavior, not a verified generation result.

The model guide says output URLs last roughly 30 minutes. Download promptly with the same Bearer header and store the bytes yourself. expires_at: null is not a permanent-retention promise. If b64_json is populated, decode it instead of downloading a URL.

Edit the saved file, not its protected URL

Inspect mug-blue.png before paying for an edit. An authenticated content_url is not necessarily public or reachable by the image provider. Send the saved PNG as a Data URI instead:

reference = Path("mug-blue.png").read_bytes() if not reference.startswith(b"\x89PNG\r\n\x1a\n"): raise SystemExit("The editing reference must be the saved PNG.") edit = { **settings, "prompt": "Change only the mug color from blue to forest green. Preserve the mug shape, " "camera angle, tabletop, background, lighting, and shadows.", "image": "data:image/png;base64," + base64.b64encode(reference).decode("ascii"), } task = api_json("POST", IMAGES + "/generations", edit) collect(task, "mug-green") 

The prompt requests preservation; it does not demonstrate that preservation happened. Review the edited file before another paid round. The entire JSON request, including Base64 encoding, must fit within 32 MiB; model-specific media limits also apply. A genuinely public, provider-reachable image URL is another accepted input option.

Keep background execution separate from recovery

Task-feature activation is required even for synchronous waiting. 403 async_not_enabled means that feature needs activation in the console. The optional part is background execution.

To use background generation, build the same generation dictionary, then use the two lines below instead of both the synchronous task = api_json(...) line and its separate collect(...) call. Choose before submitting. Do not run both versions.

task = api_json("POST", IMAGES + "/generations", {**generation, "async": True}) collect(task, "mug-blue", poll=True) 

The helper polls GET /ai/v1/images/{id} approximately every 15 seconds until a terminal state. That is the documented recommendation, not a server-enforced interval. Webhooks require async: true; a receiver is outside this tutorial.

If collection stopped, reload only the shared setup/helper block in the appendix in a fresh session if needed, including its settings definitions—not a creation block. With a saved ID, recovery uses no POST:

task_id = Path("mug-blue.task-id").read_text(encoding="utf-8").strip() task = api_json("GET", IMAGES + "/" + quote(task_id, safe="")) collect(task, "mug-blue", poll=True) 

Use the corresponding mug-green stem to recover the edit. If the creation response disappeared before an ID was saved, inspect recent tasks:

recent = api_json("GET", IMAGES + "?limit=20&order=desc") for row in recent.get("data", []): print(row.get("id"), row.get("model"), row.get("status"), row.get("created_at")) 

Match the model and creation time; do not blindly choose the newest task. Use next_after as the next request's after cursor if needed. If the match is ambiguous, contact support rather than submit again. The list is a snapshot; poll task details instead. A row's output_error means the output could not be read, not that generation produced nothing or charges changed.

Keep using the creating key for list, detail, and content access. Unified tasks are token-isolated: another key on the same account cannot read them. A local interruption does not prove the task failed or that no charge occurred.

Branch on documented error codes, not message strings

These are documented unified image errors, not observed 5.5 responses. Check HTTP status separately from task status.

Where Code Next action
HTTP 400 schema_violation Correct schema fields
HTTP 400 capability_not_supported Remove unsupported options
HTTP 504 sync_timeout Recover; contact support
HTTP 502 task_status_unavailable Do not resubmit
HTTP 422 output_blocked Revise content
HTTP 403 async_not_enabled Activate task feature
Failed task result_delivery_failed Investigate existing result

For HTTP 502 task_status_unavailable, the unified image API guide states:

The model provider task state could not be confirmed. Do not submit it again. Contact support with the task ID or request ID.

For task-level result_delivery_failed, it states:

The generated result could not be delivered. Do not resubmit automatically. Contact support with the task ID.

A sync_timeout does not establish that execution never happened. Retain error.tid, use a task ID only if one was actually returned, and ask support to resolve uncertain outcomes. Never turn an uncertain result into an automatic POST retry.

A recoverable download failure calls for retrying retrieval, not generation. Two failures need different decisions:

  • 410 artifact_expired: the artifact can no longer be retrieved. Check saved files first. If a replacement is still needed, deliberately authorize a new paid generation.
  • 429 too_many_downloads: check saved copies or contact support. Waiting alone does not guarantee renewed access.

The guide explicitly waives the generation fee for output_blocked, including when it appears in a failed task. Do not extend that exception to every failed or moderated request; output_policy_violation follows the existing moderation billing rules.

Budget for both paid actions

Sina Technology's launch report reports approximately CNY 0.28 for one 2K square edit, combining image input and output, with text tokens free. This is press reporting, not a verified official rate sheet or an AIHubMix quote.

Two paid edit rounds of an existing image, each matching that rounded scenario, illustrate approximately 2 × CNY 0.28 = CNY 0.56. That excludes initial text-to-image generation, extra references, additional candidates, and retries. It is not a measured bill.

For the example here, total = initial generation charge + reference-edit charge. A full official 5.5 tariff was not established from the examined pages, so there is no fixed total or precise old/new savings ratio to quote.

Before submitting, confirm the current model schema, activate the task feature, retain task IDs, and treat each POST as a separate paid action. Start with one image and one deliberate edit; review the saved files before adding rounds.

Appendix: shared setup and download helper

Skip this appendix when reading the workflow. Before executing the short examples, expand and load this block once into the same Python session. It configures the session and defines functions without generating an image. To recover an interrupted task in a fresh session, reload only this block—not a creation or editing block.

Expand the full helper — load once before the short examples; no image request
import base64 import os import time from pathlib import Path from urllib.parse import quote, urlsplit import requests BASE = "https://aihubmix.com" IMAGES = "/ai/v1/images" settings = { "model": "doubao-seedream-5-5", "size": "2048x2048", "n": 1, "output_format": "png", "response_format": "url", } client = requests.Session() client.headers["Authorization"] = "Bearer " + os.environ["AIHUBMIX_API_KEY"] def api_json(method, path, body=None): try: response = client.request( method, BASE + path, json=body, timeout=(15, 600), allow_redirects=False, ) except requests.RequestException: raise SystemExit( "Connection interrupted. Recover the existing task before another POST." ) from None try: data = response.json() except ValueError: raise SystemExit( f"HTTP {response.status_code}: non-JSON response; recover before another POST." ) from None if not isinstance(data, dict): raise SystemExit("Unexpected response; recover before another POST.") if not 200 <= response.status_code < 300: error = data.get("error") or {} raise SystemExit( f"HTTP {response.status_code}: code={error.get('code')}, " f"tid={error.get('tid')}. No automatic generation retry." ) return data def collect(task, stem, poll=False): task_id = task.get("id") if not isinstance(task_id, str) or not task_id: raise SystemExit("No task ID returned. Recover from the task list; do not resubmit.") Path(stem + ".task-id").write_text(task_id + "\n", encoding="utf-8") while True: status = task.get("status") print("Task:", task_id, "status:", status) if status == "completed": break if status not in {"pending", "in_progress"}: error = task.get("error") or {} raise SystemExit( f"Stopped: status={status}, code={error.get('code')}; task={task_id}." ) if not poll: raise SystemExit("Task still active. Query the saved ID; do not recreate it.") time.sleep(15) task = api_json("GET", IMAGES + "/" + quote(task_id, safe="")) output = task.get("output") or [] if not output: raise SystemExit("Completed without readable output; investigate the saved task.") item = output[0] if item.get("b64_json"): image_bytes = base64.b64decode(item["b64_json"], validate=True) else: url = item.get("content_url") if not isinstance(url, str): raise SystemExit("Missing content_url; investigate the saved task.") parsed = urlsplit(url) if (parsed.scheme, parsed.netloc) != ("https", "aihubmix.com"): raise SystemExit("Unexpected download origin; key was not forwarded.") try: response = client.get(url, timeout=(15, 120), allow_redirects=False) except requests.RequestException: raise SystemExit("Download interrupted; recover this task, not a new generation.") from None if not 200 <= response.status_code < 300: try: detail = response.json() except ValueError: detail = {} error = detail.get("error", {}) if isinstance(detail, dict) else {} code = error.get("code") if isinstance(error, dict) else None if response.status_code == 410 or code == "artifact_expired": raise SystemExit("Artifact expired; retrieval cannot recover it. Check saved files first.") if code == "too_many_downloads": raise SystemExit("Download allowance exhausted; check saved files or contact support.") raise SystemExit( f"Download HTTP {response.status_code}, code={code}; " "inspect the saved task. Retry retrieval only if recoverable." ) image_bytes = response.content if not image_bytes.startswith(b"\x89PNG\r\n\x1a\n"): raise SystemExit("Expected PNG bytes; stopped before saving or reusing the result.") destination = Path(stem + ".png") destination.write_bytes(image_bytes) print("Saved:", destination) return destination 

The 600-second read timeout is a client choice, not a service latency promise. The PNG check matches this tutorial's explicit output format; adapt it if you choose JPEG.

Keep reading: the Seedream 5.5 series

Sources

  1. Seedream 5.5 launch announcement — ByteDance Seed; vendor capability claims.
  2. Current Seedream 5.5 endpoint schema — AIHubMix; request fields and model-specific constraints.
  3. Unified image generation API — AIHubMix; tasks, downloads, and current errors.
  4. Seedream 5.5 model guide — AIHubMix; model ID and short-lived authenticated downloads; use the unified API document for route-specific errors.
  5. Seedream 5.5 launch coverage — Sina Technology; approximate editing-cost scenario, not an official tariff.