TokenHot
Home
Models
ModelsGPT-5.6Claude Opus 5Claude Fable 5Gemini 3.5 FlashClaude Sonnet 5DeepSeek V4 ProKimi K3Seedance 2.5

Providers

OpenAIAnthropicGoogleDeepSeekQwenByteDanceDoubaoMiniMaxZ.ai (GLM)
ConsoleDocumentationBlog
✓ English简体中文繁體中文日本語FrançaisРусскийTiếng Việt
TokenHot

One API. A model catalog. Usage-based billing.

Product

  • Models
  • Pricing
  • About
  • Support

Popular Models

  • GPT-5.6
  • Claude Opus 5
  • Claude Fable 5
  • Gemini 3.5 Flash
  • Claude Sonnet 5
  • DeepSeek V4 Pro
  • Kimi K3
  • Seedance 2.5

Model Providers

  • OpenAI
  • Anthropic
  • Google
  • DeepSeek
  • Qwen
  • ByteDance
  • Doubao
  • MiniMax
  • Z.ai (GLM)

Resources

  • Docs
  • Blog
  • hi@tokenhot.ai
  • Terms
  • Privacy
  • Refund Policy
© 2026 TokenHot Inc. — Built for builders.
HomeBlogAPI GuidesGPT Image 2 API: Generate, Edit, and Save Images in Python
API Guides

GPT Image 2 API: Generate, Edit, and Save Images in Python

TTokenhot Team·September 24, 2026·11 min read
GPT Image 2 API: Generate, Edit, and Save Images in Python

Cover: a conceptual illustration of generating, editing, and saving an image; it does not show outputs from a Tokenhot API test.

You can generate an image with Tokenhot's documented GPT Image 2 text-to-image route, then edit it through the separate image-to-image route. Both examples use JSON requests at https://api.tokenhot.ai/v1/images/generations, but each route has its own model ID. The Python script below saves the API response before downloading the image, so a failed download does not make you repeat the generation request.

This guide is about Tokenhot's documented gateway routes. It does not claim that their request fields or behavior match every direct OpenAI API or SDK feature. Tokenhot's Quick Start uses Bearer-token authentication and lists https://api.tokenhot.ai/v1 as its API base URL. (Quick Start)

Choose the documented route

Tokenhot currently documents three relevant GPT Image 2 routes. For a new prompt and a follow-up edit, this tutorial uses the two routes below because both point to the same documented API host and JSON generation endpoint.

Task Model ID Request
Generate from text gpt-image-2-text-to-image POST https://api.tokenhot.ai/v1/images/generations; JSON with prompt, aspect_ratio, and resolution; example uses 2K
Edit from an image URL gpt-image-2-image-to-image Same endpoint; JSON with prompt, input_urls, aspect_ratio, and resolution; example uses 2 K
Base GPT Image 2 generation gpt-image-2 Same endpoint; JSON example uses prompt, n, and size

The text-to-image documentation and image-to-image documentation show separate IDs, even though both use the generation endpoint. Keep the full ID in each request; don't substitute gpt-image-2 or a GPT Image 2.5 or sale route because the names look similar. The base gpt-image-2 doc is a separate documented route.

The two route examples also spell their resolution values differently: text-to-image shows 2K, while image-to-image shows 2 K. The rendered docs expose example payloads but no field-level schema or accepted-value list to tell whether those spellings can be interchanged. The script preserves each route's literal example; that does not establish that either value is accepted in every account or that the spellings are equivalent. Check current route documentation or Tokenhot support for your intended settings. No live request was made to test them.

The image-to-image example accepts input_urls, not a local file field. In this workflow, use the data[0].url from the first response when one is returned. If the generation response has only b64_json, the script can still save it locally, but you need to host the image somewhere the API can reach and pass that HTTPS URL for the edit. The checked docs do not document an image-upload endpoint for these routes.

Endpoint note: Tokenhot also has a separate multipart edit page for model gpt-image-2. It lists https://tokenhot.ai/v1/images/edits, while Quick Start and the generation routes list the api.tokenhot.ai host. Since the documentation does not resolve that host difference, this example does not use the multipart route. Check the current edit doc or Tokenhot support before relying on it.

Set up Python and your API key

The script uses requests to show the exact HTTP methods and payloads from the docs. It avoids assuming that a chat-completions SDK example automatically covers image routes.

python -m pip install requests

Set the key in your shell instead of pasting it into the source file. Retrieve it using the console link in Quick Start. In Bash or zsh:

export TOKENHOT_API_KEY="your-tokenhot-api-key"

In PowerShell:

$env:TOKENHOT_API_KEY = "your-tokenhot-api-key"

Complete Python implementation

Save this code as gpt_image_2_workflow.py. Each generate or edit command makes one POST when run; don't run it just to test syntax. The download command only reads a saved response and retrieves its image.

"""Generate or edit an image with Tokenhot's documented GPT Image 2 routes.

The generate and edit commands make API POSTs that require TOKENHOT_API_KEY.
The download command reads a saved response, then GETs its image URL without
an API key, or decodes a populated base64 field locally. No POST is retried.
"""

from __future__ import annotations

import argparse
import base64
import binascii
import json
import os
import sys
from pathlib import Path

import requests

API_BASE = "https://api.tokenhot.ai/v1"
TIMEOUT = (10, 300)  # connect timeout, read timeout; not a server guarantee


def response_path_for(image_path: Path) -> Path:
    return image_path.with_name(image_path.name + ".response.json")


def uncertain_path_for(image_path: Path) -> Path:
    return image_path.with_name(image_path.name + ".request-uncertain.txt")


def check_new_request_paths(image_path: Path) -> Path:
    image_path.parent.mkdir(parents=True, exist_ok=True)
    response_path = response_path_for(image_path)
    uncertain_path = uncertain_path_for(image_path)
    existing = [p for p in (image_path, response_path, uncertain_path) if p.exists()]
    if existing:
        raise FileExistsError(
            "Refusing to send a new image request because an output or request "
            f"record already exists: {', '.join(map(str, existing))}. Choose new paths "
            "or inspect the saved record first."
        )
    return response_path


def request_image(model: str, payload: dict[str, object], image_path: Path) -> dict:
    api_key = os.environ.get("TOKENHOT_API_KEY")
    if not api_key:
        raise RuntimeError("Set TOKENHOT_API_KEY in your environment before running this command.")

    response_path = check_new_request_paths(image_path)
    try:
        response = requests.post(
            f"{API_BASE}/images/generations",
            headers={"Authorization": f"Bearer {api_key}"},
            json={"model": model, **payload},
            timeout=TIMEOUT,
        )
    except requests.RequestException as exc:
        # A timeout or connection break does not tell us whether the service
        # processed the POST. Leave a marker and require a human to resolve it.
        marker = uncertain_path_for(image_path)
        marker.write_text(
            f"The POST ended without a usable HTTP response: {type(exc).__name__}: {exc}\n"
            "Do not blindly repeat this request. Check the account/request history or ask "
            "the provider before deciding whether to try again.\n",
            encoding="utf-8",
        )
        raise RuntimeError(f"Request outcome is uncertain; details saved to {marker}") from exc

    # Persist the full HTTP response before parsing JSON, checking status, or
    # downloading the image. This also prevents a rerun from repeating a POST.
    response_path.write_bytes(response.content)
    if not response.ok:
        detail = response.text[:1200].replace("\n", " ")
        raise RuntimeError(
            f"Image API returned HTTP {response.status_code}. Raw response saved to "
            f"{response_path}. Details: {detail}"
        )
    try:
        result = response.json()
    except requests.JSONDecodeError as exc:
        raise RuntimeError(f"API response was not valid JSON; raw body saved to {response_path}") from exc
    if not isinstance(result, dict):
        raise RuntimeError(f"Expected a JSON object; raw body saved to {response_path}")
    return result


def save_image_from_result(result: dict, image_path: Path) -> None:
    items = result.get("data")
    if not isinstance(items, list) or not items or not isinstance(items[0], dict):
        raise RuntimeError("No image item found in response; inspect the saved response JSON.")

    item = items[0]
    encoded = item.get("b64_json")
    if isinstance(encoded, str) and encoded:
        try:
            image_bytes = base64.b64decode(encoded, validate=True)
        except (binascii.Error, ValueError) as exc:
            raise RuntimeError("The response included invalid b64_json; keep the saved JSON for diagnosis.") from exc
        image_path.parent.mkdir(parents=True, exist_ok=True)
        with image_path.open("xb") as output:
            output.write(image_bytes)
        print(f"Saved image bytes to {image_path}")
        return

    image_url = item.get("url")
    if not isinstance(image_url, str) or not image_url.startswith(("https://", "http://")):
        raise RuntimeError("No usable data[0].url or data[0].b64_json; inspect the saved response JSON.")

    save_image_from_url(image_url, image_path)


def save_image_from_url(image_url: str, image_path: Path) -> None:
    if image_path.exists():
        raise FileExistsError(f"Refusing to overwrite {image_path}")
    image_path.parent.mkdir(parents=True, exist_ok=True)
    partial = image_path.with_name(image_path.name + ".part")
    if partial.exists():
        raise FileExistsError(f"A partial download already exists at {partial}; inspect it before continuing")
    try:
        with requests.get(image_url, stream=True, timeout=TIMEOUT) as response:
            response.raise_for_status()
            with partial.open("xb") as output:
                for chunk in response.iter_content(chunk_size=1024 * 64):
                    if chunk:
                        output.write(chunk)
        partial.replace(image_path)
    except requests.RequestException as exc:
        partial.unlink(missing_ok=True)
        raise RuntimeError(
            "The image download failed. The API response is already saved; rerun the "
            "download subcommand with that response file if its URL is still available."
        ) from exc
    print(f"Saved image to {image_path}")


def api_command(args: argparse.Namespace) -> None:
    image_path = Path(args.output)
    if args.action == "generate":
        model = "gpt-image-2-text-to-image"
        payload: dict[str, object] = {
            "prompt": args.prompt,
            "aspect_ratio": args.aspect_ratio,
            "resolution": args.resolution,
        }
    else:
        model = "gpt-image-2-image-to-image"
        payload = {
            "prompt": args.prompt,
            "input_urls": [args.input_url],
            "aspect_ratio": args.aspect_ratio,
            "resolution": args.resolution,
        }

    result = request_image(model, payload, image_path)
    response_path = response_path_for(image_path)
    print(f"Saved API response to {response_path}")
    save_image_from_result(result, image_path)


def download_command(args: argparse.Namespace) -> None:
    response_path = Path(args.response)
    image_path = Path(args.output)
    try:
        result = json.loads(response_path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        raise RuntimeError(f"Could not read a valid saved response from {response_path}") from exc
    if not isinstance(result, dict):
        raise RuntimeError(f"Expected a JSON object in {response_path}")
    save_image_from_result(result, image_path)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="action", required=True)

    for action in ("generate", "edit"):
        sub = subparsers.add_parser(action)
        sub.add_argument("--prompt", required=True, help="Text instruction for this operation")
        sub.add_argument("--output", required=True, help="New image path; existing files are not overwritten")
        if action == "generate":
            sub.add_argument("--aspect-ratio", default="16:9", help="Documented example value: 16:9")
            sub.add_argument("--resolution", default="2K", help="Documented example value: 2K")
        else:
            sub.add_argument("--aspect-ratio", default="1:1", help="Documented example value: 1:1")
            sub.add_argument("--resolution", default="2 K", help="Literal value shown in the image-to-image example")
        if action == "edit":
            sub.add_argument(
                "--input-url", required=True,
                help="HTTPS image URL reachable by the API; use data[0].url from a prior response when available",
            )
        sub.set_defaults(func=api_command)

    sub = subparsers.add_parser("download", help="Download/decode a previously saved response without a new API POST")
    sub.add_argument("--response", required=True, help="Saved response JSON from a prior generate/edit call")
    sub.add_argument("--output", required=True, help="New image path")
    sub.set_defaults(func=download_command)

    args = parser.parse_args()
    try:
        args.func(args)
    except (FileExistsError, RuntimeError, OSError) as exc:
        print(f"Error: {exc}", file=sys.stderr)
        raise SystemExit(1) from exc


if __name__ == "__main__":
    main()

Generate an image, then edit it

First ask for an image and give it a new output path. The example follows Tokenhot's text-to-image request shape: JSON to /images/generations, with model ID gpt-image-2-text-to-image, a prompt, aspect ratio, and resolution. The doc's example uses 16:9 and 2K; these are example values, not a promise about every account or route setting. (Text-to-image API example)

python gpt_image_2_workflow.py generate \
  --prompt "A paper-cut illustration of a small greenhouse at sunrise" \
  --output output/greenhouse.png

If Tokenhot returns data[0].url, the script saves the response JSON first, then streams that URL to the requested file. If the response contains nonempty data[0].b64_json instead, it decodes those bytes and saves them. The separate text-to-image and image-to-image docs show a b64_json field, but their examples leave it empty; they don't establish that this field will contain image data in a live response. The base gpt-image-2 page shows a URL. (Text-to-image response example, image-to-image response example, base model response example)

When a URL is returned, copy it from the saved response file (output/greenhouse.png.response.json) into the edit request. The image-to-image documentation shows input_urls as a list and uses a URL as the input. (Image-to-image API example)

python gpt_image_2_workflow.py edit \
  --input-url "https://example.com/greenhouse.png" \
  --prompt "Keep the greenhouse and composition; add a climbing rose along the left frame" \
  --output output/greenhouse-edited.png

Replace the example URL with the actual data[0].url from your saved generation response. If you copy the URL from elsewhere, make sure it is reachable by the API. The edit request also writes its response JSON before saving the edited file.

The two routes in the article are distinct Tokenhot model IDs. Tokenhot's docs don't establish that they share the same underlying deployment, price, parameters, or capabilities; this sequence only shows how to call the documented routes in order.

Recover without repeating a generation request

Image generation and the later file download are separate steps. If the API response arrives but the download fails, use the saved response file to retry only the download:

python gpt_image_2_workflow.py download \
  --response output/greenhouse.png.response.json \
  --output output/greenhouse-recovered.png

The output path must be new. If the image URL is no longer reachable, the response file remains available for inspection; the checked docs don't specify an expiry period. A URL download can be repeated while it remains accessible without resending the image-generation POST.

If a POST ends in a timeout or connection error before a response is received, the script writes a .request-uncertain.txt marker and stops. A client-side timeout does not prove whether the server processed the request. Check the account's request history or ask Tokenhot before deciding whether another POST is appropriate. The route docs checked here do not define an idempotency key or retry contract, so this tutorial never retries a POST automatically.

For an HTTP error response, malformed JSON, or missing output field, the raw API body remains in the .response.json file. Read that record before changing the request. The script refuses to reuse the same image/response paths for a new request, protecting the record from accidental overwrite.

Check the current route before shipping

Start with the GPT Image 2 model page and its linked API docs, then compare the route and fields with the Tokenhot Quick Start. This is a documentation-led code example, not a live request test: no paid generation or edit call was made while preparing it. Confirm current route details and account behavior in your own authorized environment before building them into a production workflow.

Back to Blog

Related Articles

Seedream 5.0 Pro API: Generate and Edit Images

Seedream 5.0 Pro API: Generate and Edit Images

September 23, 2026
Qwen3.8-Omni-Flash API: Inputs, Outputs, and Setup

Qwen3.8-Omni-Flash API: Inputs, Outputs, and Setup

September 21, 2026
Jev Explained: When to Use TypeSafe AI's Decision Model

Jev Explained: When to Use TypeSafe AI's Decision Model

September 20, 2026