OpenRouter Image Generation: A Code-First API Tutorial

OpenRouter ·

OpenRouter Image Generation: A Code-First API Tutorial

Adding image generation to an app gets harder when you need to support more than one provider. Dozens of image models across many providers use different endpoints, data formats, controls, and billing models, with charges calculated per image, megapixel, or token.

We address this integration problem with a dedicated Image generation API that uses one request format and one key across supported models.

Tl;dr

  • One API and one key reach supported image models through POST /api/v1/images.
  • Buffered responses place the generated image in data[0].b64_json, which you decode and save locally.
  • Compatible models accept an optional reference image through input_references.

In this guide, you’ll build runnable Python and JavaScript flows that send a prompt, decode and save the returned image, then pass a reference image to the endpoint and save the generated variation.

Prerequisites

Before you start, have the following ready:

  • An OpenRouter account. You’ll create the API key in Step 1.
  • Python 3 with the requests package, or Node 18+ with built-in fetch.

Step 1: Get a key and choose an image model

Create a key on the keys page, then export it in the same terminal you’ll use to run the script.

export OPENROUTER_API_KEY="sk-or-v1-..."

Choose a model from the image models collection. Let’s start with bytedance-seed/seedream-4.5 for the first run.

You can switch between image-capable models by changing the model string. Optional controls such as resolution, multiple outputs, and reference inputs vary by model, so check the capability record before adding them.

For runtime discovery, GET /api/v1/images/models returns image-capable slugs and supported parameters. Each model also has endpoint records with provider-specific capabilities and pricing. You don’t need those endpoints for this tutorial, but they’re useful once this script becomes a product feature.

Step 2: Send your first image request

Send a POST request to https://openrouter.ai/api/v1/images. The Image API requires two body fields. model selects an image-capable model, while prompt describes the image you want. Authenticate with your OpenRouter key in the Bearer header.

Python

import os
import requests

response = requests.post(
    "https://openrouter.ai/api/v1/images",
    headers={
        "Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "model": "bytedance-seed/seedream-4.5",
        "prompt": "A studio product photo of a matte black travel mug on a light gray background",
    },
    timeout=120,
)

if not response.ok:
    raise RuntimeError(f"{response.status_code}: {response.text}")

result = response.json()

Checking response.ok before parsing keeps API errors visible instead of turning them into confusing missing-field errors later.

JavaScript

const response = await fetch("https://openrouter.ai/api/v1/images", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "bytedance-seed/seedream-4.5",
    prompt: "A studio product photo of a matte black travel mug on a light gray background",
  }),
});

if (!response.ok) {
  throw new Error(`${response.status} ${await response.text()}`);
}

const result = await response.json();

The explicit error check preserves the response body, which usually contains the detail you need to fix the request.

Where is the image in the response?

A successful buffered response follows this shortened shape:

{
  "data": [
    {
      "b64_json": "iVBORw0KGgoAAA...",
      "media_type": "image/png"
    }
  ],
  "usage": {
    "cost": 0.0123
  }
}

The example cost only demonstrates the field shape. It’s not a current price.

data is an array because one request can return multiple results. The first image sits at data[0].b64_json. That value contains base64-encoded bytes, not a hosted URL. media_type appears when we can identify the output format. The optional usage.cost value reports the completed request cost when available.

For now, a nonempty b64_json value confirms that generation succeeded. The next step turns those bytes into a local image file.

Step 3: Decode and save output.png

The response shows that generation worked, but the image is still base64 text inside JSON. The next step is to decode b64_json into bytes and write those bytes to disk. Both scripts below repeat the request so you can run either file independently.

Python

Save this as generate.py:

import base64
import os
import requests

response = requests.post(
    "https://openrouter.ai/api/v1/images",
    headers={
        "Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "model": "bytedance-seed/seedream-4.5",
        "prompt": "A studio product photo of a matte black travel mug on a light gray background",
    },
    timeout=120,
)

if not response.ok:
    raise RuntimeError(f"{response.status_code}: {response.text}")

result = response.json()
images = result.get("data") or []

if not images or not images[0].get("b64_json"):
    raise RuntimeError("The response did not contain image data")

image_bytes = base64.b64decode(images[0]["b64_json"])

with open("output.png", "wb") as output_file:
    output_file.write(image_bytes)

print("Saved output.png")

cost = result.get("usage", {}).get("cost")
if cost is not None:
    print(f"Request cost: ${cost}")

base64.b64decode converts the response string into the original binary image data. Opening the destination with wb prevents Python from treating those bytes as text.

JavaScript

Save this as generate.mjs:

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

const response = await fetch("https://openrouter.ai/api/v1/images", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "bytedance-seed/seedream-4.5",
    prompt: "A studio product photo of a matte black travel mug on a light gray background",
  }),
});

if (!response.ok) {
  throw new Error(`${response.status} ${await response.text()}`);
}

const result = await response.json();

if (!result.data?.[0]?.b64_json) {
  throw new Error("The response did not contain image data");
}

await writeFile("output.png", Buffer.from(result.data[0].b64_json, "base64"));

console.log("Saved output.png");
if (result.usage?.cost !== undefined) {
  console.log(`Request cost: $${result.usage.cost}`);
}

Buffer.from(..., "base64") performs the same conversion, while writeFile saves the resulting bytes.

Run either version from the directory containing the file:

python3 generate.py

For JavaScript use:

node generate.mjs

A successful run prints Saved output.png. The cost line appears only when the response includes usage.cost.

The output format varies by model. Some models return JPEG or WebP bytes instead of PNG. If the format matters for your use, read media_type from the response and pick the file extension to match.

Step 4: Add a reference image

A reference image gives the model visual material to work from instead of relying on the prompt alone. It’s added through input_references.

For a local file, read the bytes, encode them as base64, and prepend the correct media type to create a data URL.

Place the image file product.jpg in the same project folder as the script, then create the file reference.py:

import base64
import os

import requests

with open("product.jpg", "rb") as reference_file:
    reference_base64 = base64.b64encode(reference_file.read()).decode("utf-8")

reference_data_url = f"data:image/jpeg;base64,{reference_base64}"

response = requests.post(
    "https://openrouter.ai/api/v1/images",
    headers={
        "Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "model": "openai/gpt-image-1",
        "prompt": (
            "Keep the product shape and materials. Place it on a warm stone "
            "surface with soft morning light and a clean commercial style."
        ),
        "input_references": [
            {
                "type": "image_url",
                "image_url": {
                    "url": reference_data_url,
                },
            }
        ],
    },
    timeout=120,
)

if not response.ok:
    raise RuntimeError(f"{response.status_code}: {response.text}")

result = response.json()
images = result.get("data") or []

if not images or not images[0].get("b64_json"):
    raise RuntimeError("The response did not contain image data")

variation_bytes = base64.b64decode(images[0]["b64_json"])

with open("variation.png", "wb") as output_file:
    output_file.write(variation_bytes)

print("Saved variation.png")

Run it from the same directory:

python3 reference.py

This pattern works well for product-photo variations because the source image can preserve the recognizable object while the prompt changes the setting, lighting, or presentation.

Reference-image support and accepted reference counts vary by model endpoint. Before you depend on this feature, inspect the endpoint record and confirm that input_references appears in supported_parameters.

Step 5: Make the request reusable

Once the request works reliably, move the settings that rarely change into a local configuration file. Keep the model slug, provider routing, timeout, and output directory together. Leave the prompt, reference image, and user controls in the request so they can change with each image.

Troubleshooting and cost notes

Most failures become obvious when you log the HTTP status and full response body before reading any image fields.

  • Missing data[0].b64_json. Check the response body first. Confirm that you sent a POST request to /api/v1/images and selected an image-capable model.
  • A 401 response. Make sure the process can read OPENROUTER_API_KEY. Check that the variable exists without printing its value, then export it from the terminal running the script.
  • A reference request fails. Confirm that the model supports input_references and check the image URL. A local JPEG must use a valid data URL starting with data:image/jpeg;base64,.
  • Unexpected cost. Check the endpoint’s pricing before running a batch. If available, record usage.cost with the model slug and output filename during your test runs.

Frequently asked questions

Can I use OpenRouter to generate images?

Yes. Send a POST request to /api/v1/images with an image-capable model, a prompt, and your OpenRouter API key. The response contains base64 image data that you decode and save locally.

How do I use the API to generate images?

Authorize the request with a Bearer header, then send the model and prompt. Check the response status, decode data[0].b64_json, and write the resulting bytes to an image file.

How do I generate AI images through prompts?

Describe the prompt subject, setting, composition, lighting, and style. Use input_references only when a compatible model should edit or vary an existing image.

Which API is best for image generation?

Compare image quality, controls, reference-image support, latency, and pricing. OpenRouter is the best fit when you want one API key and a common request format for multiple image models.

References