The Images API is one request, one result. POST /v1/images/generations takes JSON with a prompt; POST /v1/images/edits also takes up to 16 input images and an optional mask (Masks and References), as files, URLs or file IDs.
import fs from 'node:fs';
import OpenAI from 'openai';
const openai = new OpenAI();
const res = await openai.images.generate({
model: 'gpt-image-2.5-flare',
prompt: 'A small independent bookshop window at dusk, six books on display, '
+ 'flat vector illustration, deep blue and saffron palette, no text',
size: '1024x1024',
quality: 'low',
});
const { data: [image], usage, ...meta } = res;
fs.writeFileSync('booknest-window.png', Buffer.from(image.b64_json, 'base64'));
console.log(Object.entries(meta).map(([k, v]) => `${k}=${v}`).join(' '));
console.log(`data[0].b64_json: ${image.b64_json.length} characters`);
console.log('usage:', usage);Output
created=1790408640 background=opaque output_format=png quality=low size=1024x1024
data[0].b64_json: 1680712 characters
usage: {
input_tokens: 33,
input_tokens_details: { image_tokens: 0, text_tokens: 33 },
output_tokens: 196,
output_tokens_details: { image_tokens: 196, text_tokens: 0 },
total_tokens: 229
}The call took 13 seconds and returned a 1.26 MB PNG. The usage object is the bill: 33 prompt tokens at $5 per million plus 196 image tokens at $30 per million, $0.0060. The image is on the left of the figure below.
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
</style>
<p>Endpoint: <label><input type="radio" name="e" value="gen" checked> POST /v1/images/generations</label>
<label><input type="radio" name="e" value="edit"> POST /v1/images/edits</label> <button id="send">Send</button></p>
<canvas id="c" width="600" height="300"></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
// A stand-in for the returned PNG: a bookshop window at dusk, painted in code
function paintWindow(g, x, y, s, edited) {
const sky = g.createLinearGradient(0, y, 0, y + s);
sky.addColorStop(0, '#1f3f66'); sky.addColorStop(1, '#e09a10');
g.fillStyle = sky; g.fillRect(x, y, s, s);
g.fillStyle = '#5b3f99'; g.fillRect(x + s * 0.12, y + s * 0.2, s * 0.76, s * 0.62); // shop front
g.fillStyle = '#fff3e0'; g.fillRect(x + s * 0.2, y + s * 0.3, s * 0.6, s * 0.36); // window
['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'].forEach((c, i) => {
g.fillStyle = c; g.fillRect(x + s * (0.24 + i * 0.09), y + s * 0.42, s * 0.06, s * 0.2);
});
g.fillStyle = '#6b4f3a'; g.fillRect(x, y + s * 0.82, s, s * 0.18);
if (edited) { // the edit: a cart of books bottom left
g.fillStyle = '#8d6e63'; g.fillRect(x + s * 0.04, y + s * 0.74, s * 0.24, s * 0.1);
g.fillStyle = '#e09a10'; g.fillRect(x + s * 0.06, y + s * 0.68, s * 0.2, s * 0.06);
} else {
g.fillStyle = '#3f7d3a'; g.beginPath(); g.arc(x + s * 0.12, y + s * 0.76, s * 0.07, 0, 7); g.fill();
}
}
const usage = { gen: { text: 33, imageIn: 0, out: 196 }, edit: { text: 45, imageIn: 1024, out: 196 } };
const RATE = { text: 5, imageIn: 8, out: 30 }; // $ per million tokens
let t = 0, mode = 'gen', anim;
function box(x, y, w, h, title, lines, color) {
ctx.fillStyle = '#fff'; ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.beginPath(); ctx.roundRect(x, y, w, h, 8); ctx.fill(); ctx.stroke();
ctx.fillStyle = color; ctx.font = 'bold 12px system-ui'; ctx.fillText(title, x + 8, y + 18);
ctx.fillStyle = '#263238'; ctx.font = '11px monospace';
lines.forEach((l, i) => ctx.fillText(l, x + 8, y + 36 + i * 15));
}
function draw() {
ctx.clearRect(0, 0, 600, 300);
const req = mode === 'gen'
? ['model: gpt-image-2.5-flare', "prompt: 'A small bookshop…'", "size: '1024x1024'", "quality: 'low'"]
: ['model: gpt-image-2.5-sunburst', 'image: window.png (≤ 16)', 'mask: window-mask.png', "prompt: 'Bottom left: a cart…'"];
box(10, 20, 230, 110, 'request (JSON or multipart)', req, '#1f5f8b');
box(360, 20, 230, 110, 'OpenAI', ['one call, one result', '~13 s', 'always b64_json'], '#5b3f99');
// the packet travelling there and back
const p = Math.min(1, t / 60), q = Math.max(0, Math.min(1, (t - 100) / 60));
ctx.fillStyle = '#e09a10';
if (t < 60) { ctx.beginPath(); ctx.arc(240 + p * 120, 75, 7, 0, 7); ctx.fill(); }
if (t >= 60 && t < 100) { ctx.fillStyle = '#5b3f99'; ctx.fillText('generating' + '.'.repeat(Math.floor(t / 10) % 4), 400, 120); }
if (t >= 100 && t < 160) { ctx.fillStyle = '#3f7d3a'; ctx.beginPath(); ctx.arc(360 - q * 120, 75, 7, 0, 7); ctx.fill(); }
if (t >= 160) {
paintWindow(ctx, 10, 150, 140, mode === 'edit');
const u = usage[mode], cost = (u.text * RATE.text + u.imageIn * RATE.imageIn + u.out * RATE.out) / 1e6;
ctx.fillStyle = '#263238'; ctx.font = '12px monospace';
[`data[0].b64_json → Buffer → ${mode === 'gen' ? 'booknest-window.png' : 'window-edited.png'}`,
`usage: ${u.text} text in × $5/M`, ` + ${u.imageIn} image in × $8/M`, ` + ${u.out} image out × $30/M`,
` = $${cost.toFixed(4)}`].forEach((l, i) => ctx.fillText(l, 165, 170 + i * 20));
}
if (t < 170) { t++; anim = requestAnimationFrame(draw); }
}
function send() { cancelAnimationFrame(anim); t = 0; draw(); }
document.querySelectorAll('input[name=e]').forEach(r => r.onchange = () => { mode = r.value; send(); });
document.getElementById('send').onclick = send;
t = 170; draw(); // start by showing a finished call
</script>