ScreenshotNeo

BlogGuides

HTML to Image API for Developers

Convert HTML and CSS to reliable PNGs with hosted APIs, Playwright, or Puppeteer. Includes waits, full-page capture, troubleshooting, and production code.

By the ScreenshotNeo team1 October 20269 min read

Direct answer: An HTML-to-image API renders HTML and CSS in a browser and returns a PNG, JPEG, WebP, or PDF. Use a hosted API when you want authentication, browser maintenance, scaling, waits, and webhooks handled for you. Use Playwright or Puppeteer when you need browser-level control and are willing to run Chromium, manage dependencies, and scale workers.

For production captures, decide first whether your input is raw HTML, a public URL, or structured template data. Then set the viewport, output format, full-page behavior, device scale, timing, and failure policy. Dynamic pages usually need a selector wait, a delay, or an asynchronous webhook.

1. Choose the input model

Input Best for Important constraint
Raw HTML and CSS Invoices, cards, reports, emails, generated documents External assets must be reachable by the rendering browser, or embedded as data URLs.
Public URL Web pages, dashboards, marketing pages The renderer must be able to reach the URL without a private network, login wall, or local hostname.
Template data Repeated branded images generated from JSON The template schema and validation become part of your application contract.

Hosted services generally expose all three models. A self-hosted browser starts from HTML or a URL; your application supplies any template rendering layer.

2. Render raw HTML with a hosted API

The html2img API documents POST https://app.html2img.com/api/html for raw HTML and CSS, including inline JavaScript. It requires an API key in the X-API-Key header and can return PNG or PDF.

cURL

curl -X POST 'https://app.html2img.com/api/html' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "html": "<!doctype html><html><body><h1>Invoice</h1></body></html>",
    "css": "body { font-family: Arial, sans-serif; padding: 40px; }",
    "width": 1200,
    "height": 800,
    "format": "PNG"
  }' \
  -o output.png

Python

import requests

payload = {
    'html': '<!doctype html><html><body><h1>Invoice</h1></body></html>',
    'css': 'body { font-family: Arial, sans-serif; padding: 40px; }',
    'width': 1200,
    'height': 800,
    'format': 'PNG',
}
response = requests.post(
    'https://app.html2img.com/api/html',
    headers={'X-API-Key': 'YOUR_API_KEY'},
    json=payload,
    timeout=90,
)
response.raise_for_status()
with open('output.png', 'wb') as image:
    image.write(response.content)

Node.js

const response = await fetch('https://app.html2img.com/api/html', {
  method: 'POST',
  headers: {
    'X-API-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    html: '<!doctype html><html><body><h1>Invoice</h1></body></html>',
    css: 'body { font-family: Arial, sans-serif; padding: 40px; }',
    width: 1200,
    height: 800,
    format: 'PNG'
  })
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const image = Buffer.from(await response.arrayBuffer());
require('node:fs').writeFileSync('output.png', image);

Options that affect the render

Option Use
width, height Viewport dimensions from 1 to 5000 pixels.
fullpage Expand the capture to the document’s full height.
dpi Increase output density when needed. The documentation recommends DPI 1 for most cases because higher values use more memory and processing time.
css Inject additional CSS without changing the source document.
wait_for_selector Wait until a CSS selector exists before rendering.
ms_delay Wait a fixed number of milliseconds for animations or late content.
selector Capture a specific element for screenshot requests.
webhook_url Receive completion for slow URL captures instead of holding a synchronous request.
format Choose PNG or PDF on the documented endpoints.
scale_to_fit Fit HTML output to a PDF page.

Use wait_for_selector when a known component marks readiness. Use ms_delay only when there is no reliable readiness signal. The getting-started guide recommends synchronous requests for ordinary HTML renders and webhooks for slow URL screenshots.

3. Capture a public URL with a hosted API

For a page already deployed, use POST https://app.html2img.com/api/screenshot. The URL must be publicly accessible to the service. A private staging hostname, localhost address, or page requiring an unavailable login will fail or render an incomplete page.

curl -X POST 'https://app.html2img.com/api/screenshot' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com/dashboard",
    "width": 1440,
    "height": 900,
    "fullpage": true,
    "wait_for_selector": "main.dashboard",
    "format": "PNG"
  }' \
  -o dashboard.png

For a slow page, add webhook_url and make your receiver idempotent. Verify the webhook signature or authentication scheme documented by your provider before storing the result.

4. Use templates for repeated images

html2img documents POST https://app.html2img.com/api/v1/templates/[slug] for named templates and JSON data. Template requests return HTTP 422 for validation failures, while general validation errors use HTTP 400. Keep template versions explicit so a design change does not silently alter historical images.

5. Self-host with Playwright

Playwright gives direct control over browser launch, navigation, waits, injected styles, masking, transparency, and output files. See the official screenshot documentation for the Page API.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({
  viewport: { width: 1440, height: 900 },
  deviceScaleFactor: 1
});
await page.goto('https://example.com/dashboard', {
  waitUntil: 'networkidle',
  timeout: 60000
});
await page.locator('main.dashboard').waitFor({ state: 'visible', timeout: 30000 });
await page.screenshot({
  path: 'dashboard.png',
  fullPage: true,
  animations: 'disabled',
  mask: [page.locator('.customer-email')]
});
await browser.close();

For raw HTML, use page.setContent(html, { waitUntil: 'networkidle' }). Use page.addStyleTag({ content: css }) for injected CSS, page.locator(selector).screenshot() for one element, and omitBackground: true for a transparent PNG. JPEG quality is supported for JPEG output; PNG and WebP are lossless or format-specific.

6. Self-host with Puppeteer

Puppeteer controls Chrome and Firefox from JavaScript. Its official screenshot guide covers page and element screenshots.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com/dashboard', {
  waitUntil: 'networkidle0',
  timeout: 60000
});
await page.waitForSelector('main.dashboard', { visible: true, timeout: 30000 });
await page.screenshot({ path: 'dashboard.png', fullPage: true });
await browser.close();

To capture one component, obtain it with page.$('main.dashboard') and call element.screenshot({ path: 'dashboard-card.png' }). For deterministic output, install the browser version with your application, use fixed fonts, and disable animations in an injected stylesheet.

7. Make captures reliable

  1. Define readiness. Prefer a selector that appears only after data is loaded. Network idle can be misleading when analytics or live sockets keep requests open.
  2. Set an explicit viewport. Responsive breakpoints change layout, wrapping, and lazy-loading behavior.
  3. Handle lazy content. Scroll through long pages before a full-page capture, or use a provider option that loads lazy images.
  4. Control fonts and assets. Wait for document.fonts.ready, embed critical fonts, and avoid expiring signed asset URLs.
  5. Freeze motion. Inject CSS that sets animation and transition durations to zero.
  6. Mask sensitive data. Use Playwright masks or hide selectors before saving an image.
  7. Separate transient failures from bad pages. Retry navigation timeouts with backoff, but do not retry a deterministic 404 or validation error.
await page.addStyleTag({ content: `
  *, *::before, *::after {
    animation-duration: 0s !important;
    animation-delay: 0s !important;
    transition: none !important;
    caret-color: transparent !important;
  }
` });
await page.evaluate(() => document.fonts.ready);

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. The API documentation lists its 63 capture options.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
require('node:fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the result with X-Page-Verdict and X-Billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Options include full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets or any viewport, retina scale, PDF paper size, margins, landscape and page ranges, HTML/CSS rendering, custom CSS and JavaScript, pre-capture clicks, hidden selectors, selector or delay or network-idle waits, ad/tracker/request/resource blocking, custom headers, cookies, user agent and Authorization, timezone, geolocation, transparent backgrounds, resizing, selectable cache TTL, signed links, asynchronous jobs with signed webhooks, bulk capture of 100 URLs per call, a usage API, and an OpenAPI specification.

There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots; yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account.

9. Troubleshooting

Symptom Likely cause Fix
HTTP 400 Invalid dimensions, format, or required field. Validate the request against the provider’s parameter limits. html2img documents 1–5000 pixel width and height.
HTTP 422 on a template Template data does not match the schema. Check required fields, types, and the template slug.
Blank image JavaScript has not rendered, the URL is blocked, or the page failed. Wait for a meaningful selector, inspect browser logs, and test the URL from the renderer’s network environment.
Missing images Lazy loading, CORS, expired URLs, or blocked third-party resources. Scroll before capture, embed critical assets, refresh signed URLs, and allow required origins.
Wrong responsive layout Default viewport differs from your target. Set width, height, and device scale explicitly.
Fonts shift between runs Fallback fonts render before web fonts load. Await document.fonts.ready and package or preload the fonts.
Request times out Slow third-party requests, infinite polling, or an overly short timeout. Block unnecessary resources, use a selector wait, increase timeout within provider limits, or switch to an asynchronous webhook.
Private URL cannot be captured The hosted renderer cannot access your network. Expose a protected, temporary public URL or run Playwright/Puppeteer inside your network.
PDF content is clipped Page dimensions or fit mode do not match the document. Set paper size, margins, orientation, page ranges, or scale_to_fit.

10. Performance, reliability, and cost

  • Reduce work before increasing timeouts. Block analytics, ads, trackers, and unused resource types where your capture requirements allow it.
  • Reuse browser processes when self-hosting. A worker pool avoids paying startup cost for every request, but recycle unhealthy workers and cap concurrent pages.
  • Keep DPI at 1 by default. Higher DPI increases memory use and processing time in hosted services.
  • Cache deterministic pages. Use a content key based on URL, viewport, options, and template version. Set a TTL that matches how often the page changes.
  • Use asynchronous jobs for long pages. Webhooks prevent client request timeouts and let you retry delivery independently.
  • Price the whole system. Hosted cost is usually credits or plan usage. Self-hosting adds browser CPU and memory, container images, concurrency limits, patching, observability, and queue infrastructure.
  • Record verdicts and errors. Store status, duration, URL hash, viewport, and provider response headers so you can distinguish a bad page from a transient renderer failure.

11. Hosted API or self-hosted browser?

Decision Hosted API Playwright/Puppeteer
Operations Provider runs browsers, scaling, and maintenance. You own browser binaries, workers, patching, and capacity.
Control Use documented options, waits, masks, headers, and formats. Use browser APIs, custom scripts, network interception, and local files.
Input Often raw HTML, public URLs, and templates. HTML or URLs plus your own template layer.
Private pages Requires provider-compatible access or supplied credentials. Can run inside your private network.
Scaling Plan or credit limits define throughput. Scale workers and queues yourself.
Cost Credits or subscription terms. Infrastructure and engineering time plus browser runtime.

12. FAQ

Can an HTML-to-image API run JavaScript?

Yes. html2img’s HTML endpoint documents inline JavaScript. Wait for a selector or a deliberate delay before rendering dynamic output.

Can I capture a page that is not public?

A hosted URL endpoint normally needs public reachability. For private pages, run a browser in the same network or use a service that supports authenticated headers and cookies.

Should I use full-page mode for long documents?

Use it when the entire document is needed. For very long pages, capture sections or use PDF output to reduce image dimensions and memory pressure.

Which format should I choose?

PNG is best for crisp text and transparency, JPEG for smaller photographic images, WebP for modern web delivery, and PDF for paginated documents.

How do I avoid duplicate screenshots?

Cache by URL or HTML hash plus every visual option: viewport, device scale, color scheme, selector, wait condition, and template version.