Read the completed image_generation_call.result value from the OpenAI Responses API. That current GPT Image result is base64 image data. Decode it to PNG bytes, then upload those bytes to https://imgd.dev/v1/upload.

Last tested against the official OpenAI Responses image guide on 2026-08-20.

imgd.dev charges a one-time $1 per GB of storage and has no free tier. Fund the imgd.dev account before the first upload. OpenAI API use has a separate cost. Check the current OpenAI pricing page before the generation request.

Every imgd.dev image is public. Do not use this workflow for confidential prompts, private generated assets, unreleased designs, or personal data.

Current GPT Image output

The current OpenAI guide shows the Responses API image tool as an image_generation tool. A completed output item has type image_generation_call.

The final image bytes are in the item's result field as base64 data. The script does not look for an OpenAI image URL.

The script uses the current guide's gpt-5.6 example model by default. Set OPENAI_MODEL when another supported model fits your account.

The default GPT Image output format is PNG. The script checks the PNG signature before it uploads the bytes.

Requirements

Use Node.js 20 or later. The example uses built-in fetch, Blob, FormData, and file APIs.

Save this package file as examples/workflows/openai/package.json:

{
  "name": "imgd-openai-generated-image",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "engines": {
    "node": ">=20"
  },
  "scripts": {
    "start": "node store-generated-image.mjs"
  }
}

No external Node package is required.

Complete generation and upload script

Save this file as examples/workflows/openai/store-generated-image.mjs:

import { writeFile } from "node:fs/promises";
import { basename, extname, resolve } from "node:path";

const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
const IMGD_UPLOAD_URL = "https://imgd.dev/v1/upload";
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);

function requiredEnv(name) {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Set the ${name} environment variable.`);
  }
  return value;
}

function errorMessage(error) {
  return error instanceof Error ? error.message : String(error);
}

function isRecord(value) {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function parseJson(text, service) {
  try {
    const value = JSON.parse(text);
    if (!isRecord(value)) {
      throw new Error("The response is not a JSON object.");
    }
    return value;
  } catch (error) {
    throw new Error(`${service} returned invalid JSON: ${errorMessage(error)}`);
  }
}

async function requestJson(url, init, service) {
  const response = await fetch(url, init);
  const text = await response.text();
  return { response, payload: parseJson(text, service) };
}

async function generateImage(prompt, apiKey, model) {
  const { response, payload } = await requestJson(
    OPENAI_RESPONSES_URL,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model,
        input: prompt,
        tools: [{ type: "image_generation", action: "generate" }],
      }),
      signal: AbortSignal.timeout(180_000),
    },
    "OpenAI",
  );

  if (!response.ok) {
    const error = isRecord(payload.error) ? payload.error : {};
    const code = typeof error.code === "string"
      ? error.code
      : typeof error.type === "string"
        ? error.type
        : "unknown_error";
    const message = typeof error.message === "string" ? error.message : "Read the response body.";
    const requestId = response.headers.get("x-request-id");
    throw new Error(
      `OpenAI request failed with HTTP ${response.status} and ${code}: ${message}`
      + (requestId ? ` Request ID: ${requestId}.` : ""),
    );
  }

  const output = Array.isArray(payload.output) ? payload.output : [];
  const imageCall = output.find((item) => (
    isRecord(item)
    && item.type === "image_generation_call"
    && item.status === "completed"
    && typeof item.result === "string"
  ));
  if (!imageCall) {
    throw new Error("OpenAI returned no completed image generation result.");
  }

  const base64 = imageCall.result.replace(/\s+/g, "");
  if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) {
    throw new Error("OpenAI returned malformed base64 image data.");
  }

  const bytes = Buffer.from(base64, "base64");
  if (bytes.length === 0 || !bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
    throw new Error("OpenAI returned image data that is not a PNG.");
  }

  return bytes;
}

async function uploadImage(imageBytes, fileName, apiKey) {
  const form = new FormData();
  form.set("file", new Blob([imageBytes], { type: "image/png" }), fileName);

  const { response, payload } = await requestJson(
    IMGD_UPLOAD_URL,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${apiKey}` },
      body: form,
      signal: AbortSignal.timeout(60_000),
    },
    "imgd.dev",
  );

  if (!response.ok) {
    const code = typeof payload.error === "string" ? payload.error : "unknown_error";
    const fix = typeof payload.fix === "string" ? payload.fix : "Read the response body.";
    throw new Error(`imgd.dev upload failed with HTTP ${response.status} and ${code}: ${fix}`);
  }

  if (typeof payload.hash !== "string" || !/^[0-9a-f]{64}$/.test(payload.hash)) {
    throw new Error("The imgd.dev success response has no valid image hash.");
  }
  if (typeof payload.url !== "string") {
    throw new Error("The imgd.dev success response has no public URL.");
  }

  const publicUrl = new URL(payload.url);
  if (
    publicUrl.protocol !== "https:"
    || publicUrl.hostname !== "i.imgd.dev"
    || publicUrl.pathname !== `/i/${payload.hash}`
  ) {
    throw new Error("The imgd.dev success response has an unexpected public URL.");
  }

  return payload;
}

async function verifyPublicUrl(url) {
  const response = await fetch(url, {
    headers: { Accept: "image/*" },
    signal: AbortSignal.timeout(60_000),
  });
  const contentType = response.headers.get("content-type") ?? "";
  const _body = await response.arrayBuffer();

  if (!response.ok || !contentType.startsWith("image/")) {
    throw new Error(
      `Public URL verification failed with HTTP ${response.status} and Content-Type ${contentType}.`,
    );
  }

  console.error(`Verified ${url} with HTTP ${response.status} and Content-Type ${contentType}.`);
}

async function main() {
  const openAiKey = requiredEnv("OPENAI_API_KEY");
  const imgdKey = requiredEnv("IMGD_KEY");
  const model = process.env.OPENAI_MODEL ?? "gpt-5.6";
  const prompt = process.argv.slice(2).join(" ")
    || "Create a plain diagram with three labeled boxes: Generate, Upload, and Verify. Use blue lines on white.";
  const outputPath = resolve(process.env.OUTPUT_PATH ?? "generated-image.png");

  if (extname(outputPath).toLowerCase() !== ".png") {
    throw new Error("Use a .png OUTPUT_PATH for the default GPT Image result.");
  }

  const imageBytes = await generateImage(prompt, openAiKey, model);
  await writeFile(outputPath, imageBytes);
  const upload = await uploadImage(imageBytes, basename(outputPath), imgdKey);
  await verifyPublicUrl(upload.url);
  console.log(JSON.stringify(upload, null, 2));
}

main().catch((error) => {
  console.error(errorMessage(error));
  process.exitCode = 1;
});

writeFile closes the local file after the write. The script consumes every OpenAI and imgd.dev response body before it handles success or failure.

Run the workflow

Store both keys outside the repository. Do not print either key.

cd examples/workflows/openai
test -n "$OPENAI_API_KEY"
test -n "$IMGD_KEY"
node store-generated-image.mjs \
  "Create a simple blue line illustration of a weather station." \
  | tee upload.json

The script saves generated-image.png before the upload. Set OUTPUT_PATH when you need another PNG file path.

A new imgd.dev upload returns HTTP 202 with this complete JSON shape:

{
  "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "url": "https://i.imgd.dev/i/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "processing",
  "mime": "image/png",
  "bytes": 1845200,
  "width": 1024,
  "height": 1024,
  "alt_text": null,
  "unpublish_at": null,
  "note": "the url works immediately, serving a blurred placeholder until the moderation check finishes"
}

An identical PNG can return HTTP 200 with "deduplicated": true.

The public URL is content-addressed and works immediately. It remains stable while the image stays stored and published. Moderation can blur or remove the image.

Verify the final state

The script verifies that the public URL returns an image response. Also inspect the owner metadata:

HASH="$(jq -r '.hash' upload.json)"
curl --fail --silent --show-error \
  -H "Authorization: Bearer $IMGD_KEY" \
  "https://imgd.dev/v1/images/$HASH" | jq .

A completed live response can contain this JSON:

{
  "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "url": "https://i.imgd.dev/i/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "live",
  "mime": "image/png",
  "bytes": 1845200,
  "width": 1024,
  "height": 1024,
  "alt_text": "A blue line illustration of a compact weather station.",
  "category": "safe",
  "nsfw": false,
  "violence": false,
  "filename": "generated-image.png",
  "unpublish_at": null,
  "published": true,
  "created_at": 1787184000
}

A review result is terminal and stays blurred. A blocked result removes the bytes. Do not upload the same blocked output again.

Recover from OpenAI errors

  • HTTP 401 means OPENAI_API_KEY is absent or invalid. Correct the secret before another request.
  • HTTP 429 can mean a rate or quota limit. Read the error code and use a bounded delay.
  • HTTP 5xx is transient. Retry a small fixed number of times with backoff.
  • image_generation_user_error needs a prompt or input change. Do not retry it unchanged.
  • moderation_blocked needs a safe prompt change. Treat moderation details as optional context.

Record the OpenAI request ID for support, but never record the key or base64 image data.

Recover from imgd.dev errors

Status Meaning Recovery
401 IMGD_KEY is missing or invalid. Correct the imgd.dev secret before another upload.
402 The account has no storage. Buy storage and verify funding. There is no free tier.
402 The PNG exceeds the quota. Add the suggested storage or delete unused images.
413 The generated PNG exceeds 20 MB. Request a smaller image or compress it locally.
415 The bytes and declared type do not match. Keep the PNG signature and image/png type together.
429 The account exceeded the upload rate. Wait for the next minute, then retry.

A representative imgd.dev 415 response is:

{
  "error": "unsupported_media_type",
  "fix": "send jpeg, png, gif, webp, or avif and set the matching Content-Type",
  "accepted": ["image/jpeg", "image/png", "image/gif", "image/webp", "image/avif"]
}

Surface the imgd.dev fix value without changing it. Do not retry a 401, 413, or 415 response without the required change.

Security notes

  • Keep OPENAI_API_KEY and IMGD_KEY in separate secret values.
  • Do not put either key in source, URLs, logs, or generated image text.
  • Assume that OpenAI receives the full prompt.
  • Assume that imgd.dev serves the final image publicly.
  • Do not upload confidential generated output.
  • Do not log the base64 result because it contains the full image.
  • Remove the local PNG when local retention is not required.

When imgd.dev fits

imgd.dev fits generated images that need a reusable public URL for a report, README, or application. Identical bytes produce the same hash and URL.

It does not fit private generated assets, a free upload, video, files above 20 MB, or full digital-asset management. Use access-controlled storage for confidential output.

Next guides

For an agent, follow the integration procedure. For human setup, open the start page. Read the service evaluation before you select a host.

Sources