Keep the key in a .env file that Git 1,932 ignores and load it with Node.js 2,131 's --env-file. .withResponse() also returns the request ID that OpenAI 86 's support asks for.
// bookmark.mjs: run with node --env-file=.env bookmark.mjs
import fs from 'node:fs';
import OpenAI from 'openai';
const openai = new OpenAI({ timeout: 120_000, maxRetries: 2 }); // 2 retries is the default
try {
const { data: res, request_id } = await openai.images.generate({
model: 'gpt-image-2.5-flare-2026-09-08', // a pinned snapshot
prompt: 'A tall bookmark: a small bird on a stack of books under a crescent moon, '
+ 'flat vector illustration, deep purple and saffron, no text',
size: '512x1536', // 1:3, the tallest shape allowed
quality: 'low',
output_format: 'webp',
}).withResponse();
fs.writeFileSync('bookmark.webp', Buffer.from(res.data[0].b64_json, 'base64'));
console.log(res.size, res.output_format, `${res.usage.output_tokens} tokens`, request_id);
} catch (err) {
if (!(err instanceof OpenAI.APIError)) throw err;
console.error(`${err.status} ${err.code}: ${err.message} (request ${err.requestID})`);
process.exitCode = 1;
}Output
512x1536 webp 56 tokens req_ad27ac7206c643fd80e6cbf67fff0835
The 512x1536 bookmark cost 56 output tokens, $0.0019. With size: '500x1500' the same script printed 400 invalid_value: 400 Invalid size '500x1500'. Width and height must both be divisible by 16. and was not billed. Subclasses such as RateLimitError (429) let you retry only what is worth retrying.
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; }
.row { display: flex; gap: 12px; align-items: flex-start; }
pre { margin: 0; font-size: 11px; background: #1d2327; color: #d7f0dd; padding: 8px; width: 390px; height: 250px; overflow: auto; white-space: pre-wrap; }
canvas { display: block; }
</style>
<p>size <select id="size"><option>512x1536</option><option>500x1500</option><option>1024x1024</option></select>
simulate <select id="fault"><option value="">no fault</option><option value="429">429 rate limit</option></select> <button id="run">node --env-file=.env bookmark.mjs</button></p>
<div class="row"><canvas id="c" width="100" height="300"></canvas><pre id="term"></pre></div>
<script>
const term = document.getElementById('term'), ctx = document.getElementById('c').getContext('2d');
const say = line => { term.textContent += line + '\n'; };
// The bookmark the call returns: a small bird on a stack of books under a crescent moon
function bookmark(w, h) {
const k = 100 / w; ctx.clearRect(0, 0, 100, 300);
const s = ctx.createLinearGradient(0, 0, 0, h * k);
s.addColorStop(0, '#2d1f4f'); s.addColorStop(1, '#5b3f99');
ctx.fillStyle = s; ctx.fillRect(0, 0, w * k, h * k);
ctx.fillStyle = '#e09a10'; ctx.beginPath(); ctx.arc(62, 50, 16, 0, 7); ctx.fill();
ctx.fillStyle = '#2d1f4f'; ctx.beginPath(); ctx.arc(70, 44, 15, 0, 7); ctx.fill(); // crescent
['#e09a10', '#fff3e0', '#b5452f', '#e09a10'].forEach((c, i) => { ctx.fillStyle = c; ctx.fillRect(22 + (i % 2) * 6, 220 - i * 16, 56, 14); });
ctx.fillStyle = '#e09a10'; ctx.beginPath(); ctx.ellipse(52, 146, 14, 10, 0, 0, 7); ctx.fill(); // bird
ctx.beginPath(); ctx.arc(62, 136, 7, 0, 7); ctx.fill();
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function run() {
term.textContent = '';
const size = document.getElementById('size').value, fault = document.getElementById('fault').value;
const [w, h] = size.split('x').map(Number);
say(`$ node --env-file=.env bookmark.mjs (size: '${size}')`);
if (w % 16 || h % 16) { // validated by the API before any work
await sleep(300);
say(`400 invalid_value: 400 Invalid size '${size}'. Width and height must both be divisible by 16.`);
say('(request req_…; not billed)'); ctx.clearRect(0, 0, 100, 300); return;
}
// maxRetries: 2 → up to three attempts, with backoff, only for retryable errors
for (let attempt = 1; attempt <= 3; attempt++) {
say(`attempt ${attempt}: POST /v1/images/generations`);
await sleep(400);
if (fault && attempt < 3) { say(` 429 RateLimitError, retrying in ${0.5 * 2 ** (attempt - 1)} s`); await sleep(300); continue; }
break;
}
await sleep(500);
bookmark(w, h);
const tokens = size === '512x1536' ? 56 : 196;
say(`${size} webp ${tokens} tokens req_7f3c… ($${(tokens * 30 / 1e6 + 0.0002).toFixed(4)})`);
say('wrote bookmark.webp');
}
document.getElementById('run').onclick = run;
run();
</script>