XuanShu API

Image generation and editing examples

Use gpt-image-2 for text-to-image, image-to-image, and image editing, with runnable streaming Python and Node.js examples plus troubleshooting.

1. Get an API key

  1. If you do not have an account yet, register at /register.
  2. Create a key at /keys and replace <YOUR_API_KEY> in the examples with the full value.
  3. Check /available-channels to confirm gpt-image-2 is visible for the current key.

2. Text-to-image

Give only a prompt, with no reference image. The response streams over SSE; partial_image events report progress, and the final event carries the image as b64_json.

import base64
import json
import urllib.request
from pathlib import Path

API_URL = "https://www.xuanshuapi.com/v1/images/generations"
API_KEY = "<YOUR_API_KEY>"


def iter_sse(response):
    buffer = ""
    while chunk := response.read(4096):
        buffer += chunk.decode("utf-8", errors="replace")
        frames = buffer.split("\n\n")
        buffer = frames.pop()
        for frame in frames:
            payload = [
                line[5:].strip()
                for line in frame.splitlines()
                if line.startswith("data:")
            ]
            data = "\n".join(payload).strip()
            if data and data != "[DONE]":
                yield data


body = {
    "model": "gpt-image-2",
    "prompt": "一只在太空里漂浮的猫,科技感插画风格",
    "n": 1,
    "size": "1024x1024",
    "stream": True,
    "response_format": "b64_json",
}

request = urllib.request.Request(
    API_URL,
    data=json.dumps(body).encode("utf-8"),
    method="POST",
    headers={
        "Authorization": "Bearer " + API_KEY,
        "Content-Type": "application/json",
        "Accept": "text/event-stream",
    },
)

with urllib.request.urlopen(request, timeout=900) as response:
    for data in iter_sse(response):
        event = json.loads(data)
        if event.get("type") == "image_generation.partial_image":
            print(".", end="", flush=True)
        image = (
            event.get("b64_json")
            or ((event.get("data") or [{}])[0]).get("b64_json")
            or (event.get("item") or {}).get("result")
        )
        if image:
            Path("generated-image.png").write_bytes(base64.b64decode(image))
            print("\n已保存:generated-image.png")
            break

3. Image-to-image (JSON reference image)

Pass the reference image via images[].image_url; multiple images are supported, and data URLs also work. images[].file_id is not supported and returns an error.

import base64
import json
import urllib.request
from pathlib import Path

API_URL = "https://www.xuanshuapi.com/v1/images/edits"
API_KEY = "<YOUR_API_KEY>"


def iter_sse(response):
    buffer = ""
    while chunk := response.read(4096):
        buffer += chunk.decode("utf-8", errors="replace")
        frames = buffer.split("\n\n")
        buffer = frames.pop()
        for frame in frames:
            payload = [
                line[5:].strip()
                for line in frame.splitlines()
                if line.startswith("data:")
            ]
            data = "\n".join(payload).strip()
            if data and data != "[DONE]":
                yield data


# 参考图用 images[].image_url 传入,支持多张,也可传 data URL。
# 注意:images[].file_id 不受支持。
body = {
    "model": "gpt-image-2",
    "prompt": "参考这张图,生成一张更精致的科技风品牌图。",
    "images": [{"image_url": "https://www.xuanshuapi.com/brand/og-cover.png"}],
    "n": 1,
    "size": "1024x1024",
    "quality": "auto",
    "stream": True,
    "response_format": "b64_json",
}

request = urllib.request.Request(
    API_URL,
    data=json.dumps(body).encode("utf-8"),
    method="POST",
    headers={
        "Authorization": "Bearer " + API_KEY,
        "Content-Type": "application/json",
        "Accept": "text/event-stream",
    },
)

with urllib.request.urlopen(request, timeout=900) as response:
    for data in iter_sse(response):
        event = json.loads(data)
        if event.get("type") == "image_generation.partial_image":
            print(".", end="", flush=True)
        image = (
            event.get("b64_json")
            or ((event.get("data") or [{}])[0]).get("b64_json")
            or (event.get("item") or {}).get("result")
        )
        if image:
            Path("generated-image.png").write_bytes(base64.b64decode(image))
            print("\n已保存:generated-image.png")
            break

4. Image editing (multipart upload)

Upload an image file directly and edit it by prompt. This path uses multipart/form-data; do not set Content-Type by hand, let FormData generate the boundary.

import { writeFile } from "node:fs/promises";

const API_URL = "https://www.xuanshuapi.com/v1/images/edits";
const API_KEY = "<YOUR_API_KEY>";
const SOURCE_URL = "https://www.xuanshuapi.com/brand/og-cover.png";

async function* readSse(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const frames = buffer.split(/\r?\n\r?\n/);
    buffer = frames.pop() || "";
    for (const frame of frames) {
      const data = frame
        .split(/\r?\n/)
        .filter((line) => line.startsWith("data:"))
        .map((line) => line.slice(5).trim())
        .join("\n");
      if (data && data !== "[DONE]") yield data;
    }
  }
}

const source = await fetch(SOURCE_URL);
const sourceType = source.headers.get("content-type") || "image/png";
const sourceFile = new File(
  [await source.arrayBuffer()], "source.png", { type: sourceType });

const form = new FormData();
form.append("image", sourceFile);
form.append("prompt", "把图片整体色调改为蓝色。");
form.append("model", "gpt-image-2");
form.append("n", "1");
form.append("quality", "auto");
form.append("size", "1024x1024");
form.append("stream", "true");
form.append("response_format", "b64_json");

const response = await fetch(API_URL, {
  method: "POST",
  headers: { Authorization: "Bearer " + API_KEY, Accept: "text/event-stream" },
  body: form,
});
if (!response.ok) throw new Error(await response.text());

for await (const data of readSse(response)) {
  const event = JSON.parse(data);
  if (event.type === "image_generation.partial_image") process.stdout.write(".");
  const image = event.b64_json ?? event.data?.[0]?.b64_json ?? event.item?.result;
  if (image) {
    await writeFile("edited-image.png", Buffer.from(image, "base64"));
    console.log("\n已保存:edited-image.png");
    break;
  }
}

5. Success criteria

The command exits cleanly and writes generated-image.png or edited-image.png in the current directory, and the image opens correctly. You should also see a successful gpt-image-2 call in /usage, with no 404, 401/403, or 429.

6. Troubleshooting

SymptomCheckFix
404The text-to-image endpoint is /v1/images/generations; image-to-image and editing use /v1/images/edits. Both include /v1.Correct the URL and retry.
400 missing reference imageWhen calling edits with JSON, images[].image_url is required.Add the images array, or switch to a multipart upload of the image file.
file_id errorNeither images[].file_id nor mask.file_id is supported.Use image_url or a data URL instead.
Model unavailableCheck /available-channels to confirm gpt-image-2 is visible for the current key.Switch to an image model visible in the console.
429Check balance, key limits, and concurrency at /usage.Lower concurrency and wait for the rate limit window to reset.
No image in the responseIn streaming responses, the final image may appear in b64_json, data[0].b64_json, or item.result.Check all three locations like the examples do; write the file and break as soon as one is found.