---
title: Store OpenAI-Generated Images on a Stable Public URL
description: Read current GPT Image base64 output from the Responses API, save the PNG, upload it, and verify the public URL.
slug: store-openai-generated-images
date: 2026-08-20
updated: 2026-08-20
last_tested: 2026-08-20
summary: Decode the image_generation_call result from base64, then upload the PNG bytes to imgd.dev for a content-addressed public URL.
cluster: Automation workflows
intent: workflow
sources:
  - title: OpenAI image generation guide for the Responses API
    url: https://platform.openai.com/docs/guides/images/image-generation?api-mode=responses
  - title: OpenAI Responses API create reference
    url: https://platform.openai.com/docs/api-reference/responses/create
  - title: OpenAI API pricing
    url: https://platform.openai.com/docs/pricing
  - title: imgd.dev OpenAPI document
    url: https://imgd.dev/openapi.json
---

# Store OpenAI-Generated Images on a Stable Public URL

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`:

```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`:

```js
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.

```bash
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:

```json
{
  "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:

```bash
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:

```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:

```json
{
  "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

- [Resize or convert the generated image](/blog/resize-convert-image-webp-avif/).
- [Add the public image to Markdown](/blog/host-images-markdown-readme/).
- [Host a Puppeteer screenshot](/blog/puppeteer-screenshot-upload/).

For an agent, follow the [integration procedure](/integrate.md). For human setup, open [the start page](/#start). Read the [service evaluation](/evaluate.md) before you select a host.

## Sources

- [OpenAI image generation guide for the Responses API](https://platform.openai.com/docs/guides/images/image-generation?api-mode=responses)
- [OpenAI Responses API create reference](https://platform.openai.com/docs/api-reference/responses/create)
- [OpenAI API pricing](https://platform.openai.com/docs/pricing)
- [imgd.dev OpenAPI document](https://imgd.dev/openapi.json)
