ScreenshotNeo

BlogHow-to

How to Generate Images from Web Forms

Build a secure web-form workflow that sends prompts to an image API, supports edits and uploads, and returns downloadable images.

By the ScreenshotNeo team1 October 20268 min read

A web form can generate an image by sending the submitted prompt to your application server, having that server call an image-generation API, and returning the resulting image to the browser. Keep the API key on the server; never put it in browser JavaScript.

How the workflow works

  1. The browser renders a form for a prompt and optional settings.
  2. The browser submits the form to your backend.
  3. Your backend validates the input and calls an image API.
  4. The backend decodes or forwards the returned image data.
  5. The browser previews the image and offers a download.

For one image from one prompt, OpenAI recommends the Image API. For a conversational or multi-step editing experience, use the Responses API image-generation tool. The choice depends on the interaction your form needs.

Choose the right API shape

Requirement Use Reason
One prompt creates or edits one image Image API Direct request and response flow.
Several conversational revisions Responses API image-generation tool Preserves conversation context and supports iterative edits.
Prompt only Text field plus server request The generation endpoint accepts a prompt.
Reference image or edit Image edit endpoint or Responses image input Pass an uploaded file, URL, base64 data URL, or file ID as supported by the selected interface.

Build a complete form with Node.js

This example uses Express, the OpenAI Node SDK, and Multer for an optional reference image. It returns a data URL that the browser can display. Store the API key in the OPENAI_API_KEY environment variable as shown in the official quickstart.

1. Create the project

mkdir image-form
cd image-form
npm init -y
npm install express multer openai dotenv

2. Add the server

require('dotenv').config();
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const OpenAI = require('openai');

const app = express();
const upload = multer({ dest: 'uploads/', limits: { fileSize: 50 * 1024 * 1024 } });
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));

app.post('/api/generate', upload.single('reference'), async (req, res) => {
  const prompt = String(req.body.prompt || '').trim();
  if (!prompt) return res.status(400).json({ error: 'Prompt is required.' });
  if (prompt.length > 4000) return res.status(400).json({ error: 'Prompt is too long.' });

  try {
    const request = {
      model: 'gpt-image-1',
      prompt,
      size: req.body.size || '1024x1024',
      quality: req.body.quality || 'auto',
      output_format: req.body.format || 'png'
    };

    // Add an image edit only when a reference file was supplied.
    let result;
    if (req.file) {
      result = await client.images.edit({
        ...request,
        image: fs.createReadStream(req.file.path)
      });
    } else {
      result = await client.images.generate(request);
    }

    const item = result.data?.[0];
    if (!item?.b64_json) return res.status(502).json({ error: 'The image API returned no image data.' });
    res.json({ image: `data:image/${request.output_format};base64,${item.b64_json}` });
  } catch (error) {
    console.error('image request failed', { name: error.name, message: error.message, requestId: error.request_id });
    const status = Number.isInteger(error.status) ? error.status : 500;
    res.status(status).json({ error: 'Image generation failed. Check the server logs for the request ID.' });
  } finally {
    if (req.file) fs.promises.unlink(req.file.path).catch(() => {});
  }
});

app.listen(process.env.PORT || 3000, () => console.log('Listening on http://localhost:3000'));

Model names, supported sizes, quality values, formats, and limits can change. Confirm the current values in the Image API reference before deploying.

3. Add the form

<form id="image-form" enctype="multipart/form-data">
  <label>Prompt
    <textarea name="prompt" required maxlength="4000"></textarea>
  </label>
  <label>Size
    <select name="size">
      <option value="1024x1024">Square</option>
      <option value="1536x1024">Landscape</option>
      <option value="1024x1536">Portrait</option>
    </select>
  </label>
  <label>Reference image (optional) <input type="file" name="reference" accept="image/*"></label>
  <button>Generate</button>
</form>
<p id="status"></p>
<img id="result" alt="Generated result" hidden>
<script>
const form = document.querySelector('#image-form');
const status = document.querySelector('#status');
const result = document.querySelector('#result');
form.addEventListener('submit', async (event) => {
  event.preventDefault();
  status.textContent = 'Generating…';
  result.hidden = true;
  const response = await fetch('/api/generate', { method: 'POST', body: new FormData(form) });
  const data = await response.json();
  if (!response.ok) { status.textContent = data.error || 'Request failed'; return; }
  result.src = data.image;
  result.hidden = false;
  status.textContent = 'Done';
});
</script>

Equivalent requests

These examples call your backend route. Your server remains the only place that knows the provider credential.

cURL

curl -X POST http://localhost:3000/api/generate \
  -F 'prompt=A red fox reading beside a window, editorial illustration' \
  -F 'size=1024x1024' \
  -o result.json

Python

import requests

with open('reference.png', 'rb') as image:
    response = requests.post(
        'http://localhost:3000/api/generate',
        data={'prompt': 'Turn this into a watercolor illustration', 'size': '1024x1024'},
        files={'reference': image},
        timeout=180,
    )
response.raise_for_status()
with open('response.json', 'wb') as output:
    output.write(response.content)

Node.js

const form = new FormData();
form.append('prompt', 'A glass greenhouse on Mars at sunrise');
form.append('size', '1536x1024');
const response = await fetch('http://localhost:3000/api/generate', { method: 'POST', body: form });
if (!response.ok) throw new Error(await response.text());
const result = await response.json();
console.log(result.image.slice(0, 40));

Prompt and output design

Describe the subject, composition, style, lighting, and constraints. For edits, state what must change and what must remain. Refine one element at a time and inspect each result. Keep user-controlled prompt text separate from your system rules, and apply length limits before calling the API.

Common documented sizes are 1024×1024, 1536×1024, and 1024×1536. Output format, quality, compression, background, and custom dimensions depend on the selected model. Transparent output requires a format that supports transparency, such as PNG or WebP, where supported. Check the current model documentation for exact constraints.

Reference images and masked edits

Use an edit request when the user uploads an image. A mask identifies the editable area. The documented mask requirements include the same format and dimensions as the source image, a file under 50 MB, and an alpha channel. Validate these conditions on your server and reject unsupported files before making an API request.

Security and validation checklist

  • Keep API keys in environment variables or a secret manager.
  • Authenticate your own form endpoint and add CSRF protection where needed.
  • Limit prompt length, upload size, MIME type, and image dimensions.
  • Rate-limit submissions per user and IP.
  • Do not trust a browser-supplied filename or content type.
  • Delete temporary uploads after the request, including error paths.
  • Scan or moderate user content according to your product requirements.
  • Return generic client errors while logging provider request IDs on the server.

Performance, reliability, and cost

Image generation is slower and more expensive than ordinary form validation. Disable duplicate submissions, show progress, and set a request timeout longer than your normal page timeout. For high traffic, enqueue jobs and poll or deliver completion notifications instead of holding a browser connection open.

Cache only when the prompt, source image, settings, and user permissions match exactly. Store generated files in durable object storage rather than keeping large base64 strings in a database. Track provider status codes, latency, request IDs, output dimensions, and your own per-request cost. Retry transient server or rate-limit errors with exponential backoff and a maximum attempt count; do not blindly retry authentication or validation failures.

Troubleshooting

Symptom Likely cause Fix
401 or authentication error Missing, expired, or incorrectly loaded server key Check the environment variable and restart the server. Never move the key into frontend code.
429 or quota error Rate or usage limit Throttle clients, queue work, use bounded backoff, and check account limits.
400 for size, format, or quality Unsupported value for the selected model Use documented values for that model and validate them before the request.
Uploaded edit rejected Wrong format, dimensions, missing alpha mask, or file over the limit Normalize the source and mask server-side and enforce the documented constraints.
Blank preview Data URL MIME type does not match output Use the returned format consistently and verify that base64 data is present.
Request hangs Client timeout is too short or generation is still running Use a longer server timeout or move generation to an asynchronous job.
Intermittent 5xx Transient provider or network failure Log the request ID, retry bounded transient failures, and show a retry action.

Or skip the browser setup

If your product only needs screenshots of generated pages, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.

Use the ScreenshotNeo API documentation for the complete option list, including full-page capture, CSS selectors, device presets, retina scale, custom CSS and JavaScript, waits, blocked resources, headers, cookies, geolocation, caching, signed links, async jobs, bulk capture, and PDF settings.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also includes an MCP server so Claude, Cursor, and other MCP clients can take screenshots with take_screenshot, inspect pages with get_page_info, and create PDFs with capture_pdf. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Can I call the image API directly from browser JavaScript?

No. A browser-exposed key can be copied and abused. Send the form to your server and call the provider there.

Should every form offer image uploads?

No. Add uploads only when users need reference-based generation or editing. Prompt-only forms are simpler to validate and operate.

When should generation become asynchronous?

Use a queue when requests can exceed your web server timeout, when users submit batches, or when you need retries and progress tracking.

How do I keep generated images private?

Authorize access to stored files, avoid predictable URLs, set retention rules, and do not expose provider response data to users who lack permission.

Which settings should be user-facing?

Expose only settings your selected model supports and your users understand, usually prompt, aspect or size, format, and an optional reference image. Keep advanced controls server-configured until you can validate them safely.