Generate an image with Python

Use the Python standard library to submit one MaxAPI image task, poll its status and recover without blindly generating a second image.

Technical review:

1. Prepare a server-side key

Use Python 3.10 or later. The downloadable file uses json, time and urllib; no pip packages are required. Create a key in Console, check its allowed models and routes, and confirm available credit before running a billable request. Store MAXAPI_KEY in your server environment or secret manager, not a public notebook, browser bundle or source repository.

2. Submit a small first request

Download maxapi_image.py below and run it after setting MAXAPI_KEY securely. It sends one POST to /v1/images/generations with google/gemini-3.1-flash-lite-image, resolution 1K, aspect_ratio 1:1, n 1 and async true. These settings are deliberate: this Lite image entry is a 1K model. Do not change it to 4K without choosing a model that supports that resolution.

python3 maxapi_image.py

3. Separate acceptance from completion

The script saves no images automatically. It prints the task ID and polling URL to stderr so you can persist them in your own job record, then prints result URLs when the task succeeds. HTTP 202 means accepted, not finished; a later HTTP 200 can still describe a running or failed task. Inspect status, not only the HTTP status. Keep result URLs private if your images are private, and do not attach your MaxAPI key when downloading an image from a different host.

4. Resume, rather than submit again

Each network operation has a 30-second socket timeout; the default poll loop makes at most 120 GET requests with a five-second pause. This is not a strict ten-minute deadline because request time also counts. A polling limit or Ctrl+C does not cancel the task. Set MAXAPI_POLL_URL to the saved URL to resume with the same account key. The script will not make a new POST in resume mode. If the initial POST timed out before you received an ID, inspect request history before deciding whether another generation is needed.

MAXAPI_POLL_URL="/v1/images/generations/YOUR_TASK_ID" python3 maxapi_image.py

Download the complete example

maxapi_image.py

maxapi_image.py
"""Python 3.10+. Standard library only. Server-side; local-fixture tested, not a live model test."""
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

EXAMPLE_PAYLOAD = {
    "model": "google/gemini-3.1-flash-lite-image",
    "prompt": "A single blue ceramic cup on a plain cream background, soft studio light.",
    "resolution": "1K", "aspect_ratio": "1:1", "n": 1, "async": True,
}
ACTIVE = {"submitted", "queued", "pending", "running", "processing"}


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def safe_url(base, path):
    import re
    origin = urllib.parse.urlsplit(base)
    url = urllib.parse.urlsplit(urllib.parse.urljoin(base + "/", path))
    valid_scheme = origin.scheme == "https" or (origin.scheme == "http" and origin.hostname in {"127.0.0.1", "::1"})
    def address(value):
        return (value.scheme, value.hostname, value.port or (443 if value.scheme == "https" else 80))
    if (not valid_scheme or origin.path not in {"", "/"} or origin.query or origin.fragment
            or origin.username or origin.password or url.username or url.password or url.query or url.fragment
            or address(url) != address(origin) or not re.fullmatch(r"/v1/images/(generations|edits)(/[^/?]+)?", url.path)):
        raise RuntimeError("Unsafe API or polling URL; no credentials sent.")
    return urllib.parse.urlunsplit(url)


def run_image(key, payload=None, base="https://api.maxapi.dev", poll_url=None,
              interval=5, max_polls=120, timeout=30, on_task=lambda task: None):
    if not key:
        raise RuntimeError("Set MAXAPI_KEY in your server environment.")
    if not isinstance(max_polls, int) or not 1 <= max_polls <= 720 or interval < 0 or timeout <= 0:
        raise RuntimeError("Invalid polling limits.")
    opener = urllib.request.build_opener(NoRedirect())

    def request(path, body=None):
        import re
        headers = {"Authorization": "Bearer " + key}
        if body is not None:
            headers["Content-Type"] = "application/json"
        req = urllib.request.Request(safe_url(base, path), headers=headers,
                                     data=json.dumps(body).encode() if body is not None else None,
                                     method="POST" if body is not None else "GET")
        try:
            response = opener.open(req, timeout=timeout)
        except urllib.error.HTTPError as error:
            response = error
        except (urllib.error.URLError, TimeoutError, OSError) as error:
            raise RuntimeError("Network or timeout error; outcome may be unknown. No automatic resubmission.") from error
        with response:
            status = response.code
            try:
                result = json.load(response)
            except (ValueError, UnicodeError) as error:
                raise RuntimeError(f"HTTP {status}: non-JSON response; inspect history before resubmitting.") from error
        if not isinstance(result, dict):
            raise RuntimeError("Invalid JSON response object.")
        if not 200 <= status < 300 or result.get("error"):
            detail = result.get("error")
            code = detail.get("code", "api_error") if isinstance(detail, dict) else "api_error"
            code = code if isinstance(code, str) and re.fullmatch(r"[a-zA-Z0-9_-]{1,80}", code) else "api_error"
            raise RuntimeError(f"HTTP {status}: {code}; no automatic retry.")
        return result

    current = request(poll_url) if poll_url else request("/v1/images/generations", EXAMPLE_PAYLOAD if payload is None else payload)
    path = poll_url or current.get("poll_url")
    if path:
        path = safe_url(base, path)
        on_task({"id": current.get("id"), "poll_url": path})
    count = 0
    while True:
        if current.get("status") == "succeeded" or (not current.get("status") and isinstance(current.get("data"), list) and current["data"]):
            return current
        if current.get("status") not in ACTIVE:
            raise RuntimeError("Task failed, cancelled or returned an unknown state. Check history; no resubmission.")
        if not path:
            raise RuntimeError("Pending task has no polling URL. Check history; do not resubmit automatically.")
        if count >= max_polls:
            raise RuntimeError("Polling limit reached; task was NOT cancelled. Resume using the saved polling URL.")
        time.sleep(interval)
        current = request(path)
        count += 1


if __name__ == "__main__":
    try:
        result = run_image(os.environ.get("MAXAPI_KEY"), poll_url=os.environ.get("MAXAPI_POLL_URL"),
                           on_task=lambda task: print("Save to resume:", json.dumps(task), file=sys.stderr))
        print(json.dumps({"id": result.get("id"), "status": result.get("status", "succeeded"),
                          "urls": [item["url"] for item in result.get("data", []) if item.get("url")]}, indent=2))
    except (RuntimeError, ValueError) as error:
        print(str(error), file=sys.stderr)
        sys.exit(1)

Model-specific parameters

Public configuration snapshot; not live availability. Follow each model page for its parameters, supported routes and reference limits.

References

Continue your integration

Manage API keysAPI referenceCheck current prices