
If you are wiring image generation into a real product, the OpenAI Image API is probably where you start. Getting a single picture back is straightforward. Keeping the feature fast, affordable, and predictable once real users hit it is the harder part. This guide is for backend and full-stack developers who already have an OpenAI key and now need patterns that survive production traffic.
You will learn which model to pick, how to structure prompts so results stay consistent, how to stream partial images for better perceived speed, how to edit instead of regenerate, and how to keep costs from drifting. Along the way, we will cover the parts that bite teams later: base64 handling, moderation rejections, and retry behavior.
What Is the OpenAI Image API?
The OpenAI Image API is a set of HTTP endpoints for generating and editing images from text prompts. It exposes /v1/images/generations for new images, /v1/images/edits for modifying existing ones, and returns base64-encoded image data rather than a hosted URL. Current models are the GPT Image family, led by gpt-image-2.
That last detail catches people migrating from older code. Furthermore, it changes your architecture: you own storage from the first request.
Which Image Model Should You Use?
Model choice is mostly a cost and latency decision, because the API surface is nearly identical across the family. Start with gpt-image-2 unless you have a specific reason not to.
| Model | Status | Best for |
|---|---|---|
gpt-image-2 | Current default | New builds, highest fidelity, flexible resolutions |
gpt-image-1 | Available | Existing integrations not yet migrated |
gpt-image-1.5 | Retiring December 1, 2026 | Migrate to gpt-image-2 |
gpt-image-1-mini | Retiring December 1, 2026 | Migrate to gpt-image-2 |
dall-e-3 | Shut down May 12, 2026 | No longer accepts requests |
dall-e-2 | Shut down May 12, 2026 | No longer accepts requests |
Two dates matter here. First, OpenAI retired dall-e-2 and dall-e-3 on May 12, 2026, so any code still sending those model strings is returning errors today. Second, gpt-image-1.5, gpt-image-1-mini, and chatgpt-image-latest are scheduled for shutdown on December 1, 2026, with gpt-image-2 as the recommended replacement. Check the OpenAI deprecations page before pinning a model in config.
Consequently, the safest default for anything you expect to run past this year is gpt-image-2.
Setting Up Your First Generation Call
Install the official SDK and set OPENAI_API_KEY in your environment. If you have not used the platform before, our guide on building apps with the OpenAI API covers auth and client setup in more depth.
# pip install openai
import base64
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
result = client.images.generate(
model="gpt-image-2",
prompt=(
"A ceramic pour-over coffee dripper on a walnut counter, "
"morning window light from the left, shallow depth of field, "
"product photography, neutral background"
),
size="1024x1024",
quality="medium",
)
# The Image API returns base64 payloads, not hosted URLs.
image_bytes = base64.b64decode(result.data[0].b64_json)
with open("dripper.png", "wb") as f:
f.write(image_bytes)
Why this works: result.data is a list because n can be greater than one, and each entry carries b64_json. Notably, there is no url field to fall back on with GPT Image models, so decoding is mandatory rather than optional.
The Node client mirrors the same shape:
// npm install openai
import OpenAI from "openai";
import fs from "node:fs";
const openai = new OpenAI();
const result = await openai.images.generate({
model: "gpt-image-2",
prompt: "A ceramic pour-over coffee dripper on a walnut counter, morning window light",
size: "1024x1024",
quality: "medium",
});
const imageBytes = Buffer.from(result.data[0].b64_json, "base64");
fs.writeFileSync("dripper.png", imageBytes);
Pattern 1: Write Prompts Like a Creative Brief
Vague prompts produce inconsistent output, and inconsistency is what makes image features feel broken to users. Therefore, treat the prompt as a structured brief rather than a sentence.
A reliable structure covers five things in order:
- Subject — what is actually in the frame
- Composition — camera angle, crop, and placement
- Lighting — direction, quality, and time of day
- Style — medium, era, or reference genre
- Constraints — background, color palette, and what to exclude
PROMPT_TEMPLATE = """
Subject: {subject}
Composition: {composition}
Lighting: {lighting}
Style: {style}
Constraints: {constraints}
"""
prompt = PROMPT_TEMPLATE.format(
subject="a single running shoe, side profile",
composition="centered, full product in frame, generous margins",
lighting="soft studio softbox, no harsh shadows",
style="clean e-commerce product photography",
constraints="pure white background, no text, no logos, no people",
)
Why this works: templating the brief means each field becomes a variable your application controls. As a result, you can hold style and lighting constant across a whole catalog while varying only the subject, which is exactly what visual consistency requires.
GPT Image models render legible text far better than earlier generations, so put exact strings in quotes when you need them. For broader prompting technique, our post on prompt engineering best practices applies here too, since the same specificity rules carry over from text to images.
Pattern 2: Treat Size and Quality as Cost Levers
The quality and size parameters do more than change how the picture looks. In practice, they are your primary spend controls, because billing is token-based and larger, higher-quality renders consume more image output tokens.
| Parameter | Allowed values | Default | Notes |
|---|---|---|---|
size | auto, 1024x1024, 1536x1024, 1024x1536, and other WIDTHxHEIGHT values | auto | Dimensions must be multiples of 16, within a 3:1 aspect ratio, max edge 3840px |
quality | auto, low, medium, high | auto | Lower tiers render faster and cost less |
output_format | png, jpeg, webp | png | WebP or JPEG shrink payloads substantially |
output_compression | 0–100 | — | Applies to JPEG and WebP output |
background | auto, opaque | auto | Request PNG or WebP when you need transparency support |
moderation | auto, low | auto | Controls content filtering strictness |
n | integer | 1 | Number of images per request |
partial_images | 0–3 | 0 | Number of progressive previews when streaming |
A practical rule: generate previews at low quality, then re-render only the option the user actually selects at high. Most users discard most candidates, so paying full price for every draft wastes the majority of your spend.
def generate_candidates(prompt: str, count: int = 4):
"""Cheap drafts for selection; the winner gets re-rendered at high quality."""
return client.images.generate(
model="gpt-image-2",
prompt=prompt,
n=count,
quality="low",
size="1024x1024",
output_format="webp",
output_compression=80,
)
Why this works: webp with compression cuts the base64 payload you move across your network and store, while low quality reduces the output tokens billed. Consequently, the draft stage becomes a rounding error compared with the final render. Because pricing changes, confirm current rates on the OpenAI pricing page rather than hardcoding cost assumptions; the same budgeting discipline in our guide to token counting and budget management for LLM apps applies directly to image workloads.
Pattern 3: Stream Partial Images to Hide Latency
Image generation takes seconds, not milliseconds. Meanwhile, a spinner that sits still for eight seconds reads as a hung request. Streaming partial images fixes the perception problem without making generation any faster.
stream = client.images.generate(
model="gpt-image-2",
prompt="A river made of white owl feathers through a winter landscape",
stream=True,
partial_images=2,
)
for event in stream:
if event.type == "image_generation.partial_image":
preview = base64.b64decode(event.b64_json)
push_to_client(preview, index=event.partial_image_index) # your transport
elif event.type == "image_generation.completed":
final = base64.b64decode(event.b64_json)
persist(final)
Why this works: each image_generation.partial_image event carries a progressively refined render plus a partial_image_index, so the browser can swap in a sharper version as it arrives. Users see motion within the first second or two. Importantly, partial_images accepts 0 through 3, and each preview is billed, so two is usually the sweet spot between feedback and cost.
Forward these events to the browser over Server-Sent Events or WebSockets rather than buffering them server-side, since buffering defeats the entire purpose.
Pattern 4: Edit Instead of Regenerating
When a user wants one thing changed, regenerating from scratch throws away everything that was already right. The edits endpoint keeps the rest of the image intact.
result = client.images.edit(
model="gpt-image-2",
image=open("lounge.png", "rb"),
mask=open("mask.png", "rb"),
prompt="A sunlit indoor lounge with a pool containing a flamingo",
)
edited = base64.b64decode(result.data[0].b64_json)
The mask is where most implementations go wrong. Specifically, the mask must share the same format and dimensions as the source image, stay under 50MB, and contain an alpha channel. Transparent pixels in the mask mark the region the model may repaint; opaque pixels are preserved.
from PIL import Image
def build_mask(source_path: str, box: tuple[int, int, int, int]) -> str:
"""Punch a transparent rectangle into an otherwise opaque mask."""
src = Image.open(source_path)
mask = Image.new("RGBA", src.size, (0, 0, 0, 255)) # fully opaque = keep
mask.paste((0, 0, 0, 0), box) # transparent = editable
out_path = "mask.png"
mask.save(out_path)
return out_path
Why this works: generating the mask programmatically from user-supplied coordinates avoids the classic bug where a mask drawn in an image editor silently loses its alpha channel on export. That failure mode is frustrating to debug, because the API accepts the file and simply edits the wrong region.
Pattern 5: Guide Style With Reference Images
Text alone struggles to pin down a visual identity. Describing your brand’s exact shade of teal and its particular flat-illustration style takes a paragraph, and the model still approximates it. Passing reference images instead of adjectives is far more reliable.
The edits endpoint accepts one or more input images used as visual guidance rather than as a canvas to inpaint. In other words, you supply examples and the model generates something new in that vein.
def generate_in_house_style(subject: str, style_refs: list[str]):
"""Generate new artwork that matches an existing brand style."""
return client.images.edit(
model="gpt-image-2",
image=[open(path, "rb") for path in style_refs], # no mask: guidance only
prompt=(
f"Create a new illustration of {subject}. "
"Match the color palette, line weight, and flat vector style "
"of the reference images. Do not copy their composition."
),
size="1536x1024",
quality="high",
)
Why this works: omitting the mask argument changes the semantics of the call. With a mask, the model repaints a region of the first image; without one, the inputs act as style references for a fresh render. Therefore the same endpoint covers two distinct jobs, which is easy to miss when scanning the docs.
Keep the reference set small and coherent. Mixing three unrelated styles gives the model contradictory signals, and the result usually lands somewhere unhelpful between them. Two or three tightly matched examples outperform a dozen loosely related ones.
Pattern 6: Use the Responses API for Multi-Turn Editing
The Image API handles one prompt at a time. However, conversational experiences (“make it warmer”, “now remove the scarf”) need continuity across turns. For that, use the image_generation tool through the Responses API.
response = client.responses.create(
model="gpt-5.6", # any GPT-5-series or newer model supporting the tool
input="Generate a gray tabby cat hugging an otter with an orange scarf",
tools=[{"type": "image_generation"}],
)
images = [item.result for item in response.output if item.type == "image_generation_call"]
# The follow-up inherits full context from the prior turn.
followup = client.responses.create(
model="gpt-5.6",
previous_response_id=response.id,
input="Now make it photorealistic and drop the scarf",
tools=[{"type": "image_generation"}],
)
Why this works: previous_response_id carries the conversation forward, so the model already knows what “it” refers to. Otherwise you would have to re-describe the entire image on every turn, which reliably causes drift.
Choose deliberately between the two entry points. Use the Image API for single-shot generation in a pipeline; use the Responses API when a human is iterating in a loop.
Pattern 7: Move Base64 Out of Your Process Immediately
A 1536×1024 PNG can run to several megabytes, and base64 inflates that by roughly a third. Holding those strings in application memory, JSON responses, or your database is the fastest way to turn a working feature into an outage.
import base64, hashlib, boto3
s3 = boto3.client("s3")
def store_generated_image(b64_payload: str, bucket: str, ext: str = "png") -> str:
raw = base64.b64decode(b64_payload)
key = f"generated/{hashlib.sha256(raw).hexdigest()}.{ext}"
s3.put_object(
Bucket=bucket,
Key=key,
Body=raw,
ContentType=f"image/{ext}",
CacheControl="public, max-age=31536000, immutable",
)
# Hand the client a short-lived URL; never the base64 blob.
return s3.generate_presigned_url(
"get_object", Params={"Bucket": bucket, "Key": key}, ExpiresIn=3600
)
Why this works: hashing the decoded bytes gives you content-addressed keys, so identical renders deduplicate for free and the immutable cache header becomes safe. Additionally, returning a presigned URL keeps your API responses small. Our post on AWS S3 best practices for security, performance, and cost covers bucket policies and lifecycle rules worth applying to generated assets, which accumulate quickly.
Pattern 8: Separate Moderation Failures From Rate Limits
Two very different failures look similar in logs if you catch them together. Moderation rejections are permanent for that prompt, whereas rate limits are temporary. Retrying the first wastes money and never succeeds.
from openai import BadRequestError, RateLimitError, APIStatusError
import time
def generate_with_retry(prompt: str, attempts: int = 4):
for attempt in range(attempts):
try:
return client.images.generate(model="gpt-image-2", prompt=prompt)
except BadRequestError as e:
# Content policy rejection: deterministic. Surface it, do not retry.
raise ContentRejected(str(e)) from e
except (RateLimitError, APIStatusError) as e:
if attempt == attempts - 1:
raise
time.sleep(2 ** attempt) # exponential backoff
Why this works: the SDK raises BadRequestError for policy violations and RateLimitError for 429s, which lets you branch on cause rather than status code alone. Meanwhile, a bare except Exception with a retry loop will hammer the API with a prompt that can never pass. For queueing and backoff strategy across providers, see our breakdown of LLM rate limiting and retry strategies.
The moderation parameter accepts auto or low. Lowering it loosens filtering somewhat, but it does not disable policy enforcement, so build the rejection path regardless.
Real-World Scenario: Catalog Imagery at Scale
Consider a small e-commerce team adding lifestyle imagery to a catalog of several thousand SKUs, working through the backlog over a few weeks. The naive approach is one high-quality render per product, generated on demand when a page is first viewed.
That design fails in two predictable ways. First, cost scales with page views rather than with catalog size, because nothing is cached and popular products get regenerated repeatedly. Second, the first visitor to any product page waits several seconds for a synchronous render, which is exactly the wrong moment to add latency.
The version that holds up inverts both assumptions. Generation moves to an offline batch job keyed by SKU, results land in object storage behind a CDN, and the page serves a static asset. Moreover, the brief template stays fixed across the catalog so lighting and framing remain consistent from product to product.
The trade-off is real and worth naming: batch generation means new SKUs have no imagery until the job runs, so the team needs a placeholder strategy and a trigger for late additions. In exchange, per-image cost becomes a one-time expense instead of a recurring one, and page latency stops depending on the OpenAI Image API at all.
When to Use the OpenAI Image API
- You need strong prompt adherence and legible text rendered inside images
- Your product already runs on OpenAI models and you want one vendor and one key
- You need conversational, multi-turn editing through the Responses API
- Masked inpainting on user-supplied images is a core feature
- Generation volume is moderate and predictable enough to budget
When NOT to Use the OpenAI Image API
- You need on-premise or air-gapped generation, where a self-hosted diffusion model fits better
- Your volume is high enough that per-image cost dominates and open-weight models on rented GPUs win
- You require fine-grained control over sampling steps, seeds, LoRAs, or ControlNet-style conditioning
- The content sits close to policy boundaries and moderation rejections would block a core workflow
- You mainly need image understanding rather than generation, where the Gemini multimodal vision and video API is worth evaluating alongside OpenAI
Common Mistakes with the OpenAI Image API
- Expecting a
urlfield in the response and shipping code that silently breaks, since GPT Image models return onlyb64_json - Storing base64 strings in a database column instead of decoding to object storage
- Retrying content-policy rejections with backoff, which burns quota on a request that will never succeed
- Generating every candidate at
highquality when users discard most of them - Building masks in an image editor that strips the alpha channel, so edits apply to the wrong region
- Hardcoding a model string in application code rather than config, which turned into an outage for teams still sending
dall-e-3after May 12, 2026 - Regenerating from scratch for small changes instead of using the edits endpoint or
previous_response_id
Conclusion
The OpenAI Image API rewards treating image generation as infrastructure rather than a single API call. Structure your prompts as briefs, use quality tiers as the cost lever they are, stream partials so the wait feels shorter, and get base64 out of your process the moment it arrives.
Start with one concrete change today: audit your codebase for any hardcoded dall-e-2 or dall-e-3 model strings and move model selection into configuration. From there, add the low-quality draft plus high-quality final pattern, which typically cuts spend more than any other single adjustment. Next, read our guide on prompt engineering best practices to tighten the briefs feeding your generation pipeline.