Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

You can convert HTML to a PNG by sending it to a small API that renders it in a headless browser and returns the screenshot bytes. The example below uses Node.js, Express, and Playwright; it accepts HTML, viewport dimensions, and an optional full-page setting at POST /api/screenshot. “GitHub API” here means an API you can put in a GitHub repository and run yourself—not an endpoint hosted by GitHub.

What this API does—and what you need

A browser screenshot is a rendered image, not a direct conversion of HTML source. The service loads the markup in Chromium, lays it out at a specified viewport size, and captures the pixels. That means browser rendering rules apply: fonts, CSS, images, and page scripts can affect the result. For a predictable service, the example below disables page JavaScript and blocks external network requests. Put any required images or fonts directly in the HTML as data URLs, or adapt the network policy deliberately.

You will need Node.js, npm, and a machine or container that can run Playwright’s Chromium browser. The code uses an Express JSON endpoint and returns raw PNG or JPEG bytes with the matching content type. Playwright can also return screenshot buffers for further processing, store captures in a file, capture a full page, or capture a locator rather than the whole page.

Create the project and install dependencies

  1. Create a directory and initialize a Node.js project: mkdir html-image-api && cd html-image-api && npm init -y.

    Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  2. Install Express and Playwright: npm install express playwright.

  3. Install Playwright’s Chromium browser: npx playwright install chromium. On Linux systems, Playwright may also require system packages; use its install command with the dependency option supported by your environment if Chromium reports missing libraries.

  4. Save the following server as server.js.

  5. Start it with node server.js. It listens on port 3000 by default, or the port in the PORT environment variable.

Runnable Node.js API server

This implementation accepts JSON with html, optional width and height, optional fullPage, and optional format (png or jpeg). It limits the body size and viewport range, sets a rendering timeout, disables page JavaScript, blocks remote requests, and closes each browser context after use.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const express = require('express');
const { chromium } = require('playwright');

const app = express();
const port = Number(process.env.PORT || 3000);
const MAX_HTML_BYTES = 1024 * 1024;
const MAX_WIDTH = 2400;
const MAX_HEIGHT = 4000;

app.use(express.json({ limit: MAX_HTML_BYTES }));

let browser;

function validDimension(value, fallback, max) {
  if (value === undefined) return fallback;
  return Number.isInteger(value) && value >= 1 && value <= max;
}

app.post('/api/screenshot', async (req, res) => {
  const { html, width = 1280, height = 800, fullPage = false, format = 'png' } = req.body || {};

  if (typeof html !== 'string' || html.length === 0) {
    return res.status(400).json({ error: 'html must be a non-empty string' });
  }
  if (!validDimension(width, 1280, MAX_WIDTH) || !validDimension(height, 800, MAX_HEIGHT)) {
    return res.status(400).json({ error: `width must be 1-${MAX_WIDTH} and height 1-${MAX_HEIGHT}` });
  }
  if (typeof fullPage !== 'boolean') {
    return res.status(400).json({ error: 'fullPage must be a boolean' });
  }
  if (!['png', 'jpeg'].includes(format)) {
    return res.status(400).json({ error: 'format must be png or jpeg' });
  }

  let context;
  try {
    context = await browser.newContext({
      viewport: { width, height },
      javaScriptEnabled: false
    });

    // Permit embedded data/blob assets; block requests to other origins.
    await context.route('**/*', (route) => {
      const url = route.request().url();
      if (url.startsWith('data:') || url.startsWith('blob:') || url === 'about:blank') {
        return route.continue();
      }
      return route.abort();
    });

    const page = await context.newPage();
    await page.setContent(html, { waitUntil: 'domcontentloaded', timeout: 15000 });

    const image = await page.screenshot({
      type: format,
      fullPage,
      timeout: 15000,
      ...(format === 'jpeg' ? { quality: 85 } : {})
    });

    res.set('Content-Type', format === 'png' ? 'image/png' : 'image/jpeg');
    res.set('Cache-Control', 'no-store');
    return res.status(200).send(image);
  } catch (error) {
    console.error('Screenshot request failed:', error.message);
    if (!res.headersSent) {
      return res.status(500).json({ error: 'Could not render the supplied HTML' });
    }
  } finally {
    if (context) await context.close().catch(() => {});
  }
});

app.use((error, req, res, next) => {
  if (error.type === 'entity.too.large') {
    return res.status(413).json({ error: 'Request body is too large' });
  }
  return res.status(400).json({ error: 'Invalid JSON request body' });
});

async function start() {
  browser = await chromium.launch({ headless: true });
  app.listen(port, () => console.log(`Screenshot API listening on port ${port}`));
}

async function stop() {
  if (browser) await browser.close();
  process.exit(0);
}

process.on('SIGINT', stop);
process.on('SIGTERM', stop);
start().catch((error) => {
  console.error('Could not start Chromium:', error);
  process.exit(1);
});

The handler sends a buffer directly, so the caller receives an image rather than JSON containing a base64 string. That is usually preferable when the next step is saving the image or forwarding it to object storage. If a client specifically requires JSON, encode the returned buffer as base64 and include an explicit format field; remember that encoding increases payload size.

Call the endpoint and save the image

For a quick test, save a JSON request as request.json:

{
  "html": "<!doctype html><html><body><h1>Hello, image</h1><p>Rendered by Chromium.</p></body></html>",
  "width": 1200,
  "height": 800,
  "fullPage": false,
  "format": "png"
}

Then post it and write the returned bytes to a file:

curl -X POST http://localhost:3000/api/screenshot 
  -H 'Content-Type: application/json' 
  --data-binary @request.json 
  -o screenshot.png

To request the full scrollable page, set "fullPage": true. For JPEG output, set "format": "jpeg"; the example uses quality 85 for JPEG. The quality option is not applied to PNG. After a successful request, screenshot.png should be a valid image rather than an HTML or JSON response.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the right screenshot options

Need Approach Trade-off or detail
Capture the visible viewport Leave fullPage false. The output is bounded by the requested width and height.
Capture the complete scrollable page Set fullPage true. Very long documents can produce large images and use more memory.
Capture one card, chart, or component Use a Playwright locator and its screenshot method, for example await page.locator('.card').screenshot(). Make sure the locator exists and is visible before capturing; element screenshots are distinct from full-page captures.
Return bytes for another service Keep the screenshot result as a buffer and pass it to storage or image-processing code. A buffer avoids writing a temporary file; base64 is useful only when the receiving interface requires text.
Set a crop or output format Use screenshot options for image type, quality where applicable, or a clip rectangle. Playwright’s screenshot API documents format, quality, and clipping controls. This sample intentionally exposes only PNG/JPEG and viewport/full-page capture.

Playwright also supports saving directly to a path with page.screenshot({ path: 'screenshot.png' }). Its documented API includes locator screenshots and full-page captures. Puppeteer offers a similar page screenshot method, with options including fullPage, clip, encoding, omitBackground, path, quality, and type; PNG ignores its documented quality parameter. Choose one browser automation library based on your runtime and operational needs rather than assuming either is universally faster or more accurate. The cited documentation does not establish a fair speed or image-fidelity winner.

Secure and operate the service responsibly

Rendering untrusted HTML is not harmless just because it is headless. The browser parses attacker-controlled markup and styles, and a permissive server can expose network or machine resources. The sample’s JavaScript-off and outbound-request blocking policies reduce some risk, but they are not a substitute for proper isolation.

  • Keep the browser sandbox enabled. Do not add Chromium’s no-sandbox flag as a quick deployment fix. Configure the container or host so Chromium can use its sandbox.
  • Isolate workers. Run the renderer with restricted filesystem access, low privileges, and resource limits. Avoid mounting secrets or sensitive host paths into the browser container.
  • Set request and concurrency limits. A body-size cap and dimension bounds are included, but a public deployment also needs authentication or another access policy, rate limits, and a bounded queue so many simultaneous captures cannot exhaust memory.
  • Control external resources intentionally. This version blocks network fetches to avoid arbitrary HTML retrieving remote or internal resources. If you need remote fonts or images, allow only specific trusted hosts and block private, loopback, link-local, and cloud metadata destinations. Re-check redirects and resolved addresses, not just the initial URL.
  • Watch time and output size. The sample sets browser navigation/screenshot timeouts and maximum viewport dimensions. Add process-level limits, request deadlines, and limits on generated image bytes for your deployment.
  • Plan for browser lifecycle failures. Monitor worker exits and restart the service or replace the browser process after a crash. A production queue should reject or defer work when the worker pool is full.

The code uses one browser process and creates a fresh context per request, then closes that context. This avoids launching a whole browser for every image while keeping cookies and page state separated. Under load, benchmark your own workload for memory, capture time, and queue depth; no general performance number follows from the API documentation alone.

Troubleshoot common failures

Symptom Likely cause What to check
Chromium fails to launch Browser binary or operating-system dependencies are missing, or the runtime blocks the sandbox. Run npx playwright install chromium, install the required Linux browser dependencies, and configure a sandbox-capable environment.
HTTP 400 response Malformed JSON, missing/non-string HTML, invalid dimensions, non-boolean fullPage, or an unsupported format. Check the request fields and use positive integer viewport dimensions within the server limits.
HTTP 413 response The JSON body exceeds the Express body-size limit. Reduce markup size or deliberately raise the limit while maintaining memory and request controls.
Image lacks fonts, images, or styles Remote resources are blocked by design, or the HTML does not include the required styles. Embed assets as data URLs or implement a strict allowlist for trusted origins; verify the CSS is in the submitted document.
Capture times out Rendering did not settle before the configured timeout, or the browser is overloaded. Reduce document complexity, tune a timeout within a bounded budget, and inspect worker load. Do not wait forever for network idle on content that continually fetches.
Image is unexpectedly tall or memory use spikes A very long full-page document creates a large bitmap. Use viewport capture, reduce content or dimensions, and enforce maximum output and worker memory limits.
Client saves an error page as an image The response is JSON with an error status, but the client always writes the body to a .png file. Check the HTTP status and Content-Type before treating the response as image bytes.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your HTML is already hosted at a URL, ScreenshotNeo can return a screenshot without your operating a Chromium worker. Its one-request API captures a URL; it is not a replacement for this endpoint when your only input is an unsaved HTML string. See the ScreenshotNeo website and API documentation for its request options and response details.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers say which page verdict occurred and whether it was billed. It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000 screenshots, and every feature is on every plan. Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

When to build it yourself

A self-hosted Playwright endpoint fits when the input is raw HTML, you need direct control over browser behavior, or you want to integrate rendering into an existing application. A managed screenshot API fits when the input is a reachable page URL and you would rather not maintain browser workers. For either approach, define what “ready” means for your page, set capture and resource limits, and verify the output format and billing or hosting costs against your expected workload.

Frequently Asked Questions

Does GitHub run the screenshot API when I push this code to a repository?

No. A GitHub repository stores the source; you still need to run the Node.js service on a machine or deployment platform that can run Chromium.

Can I submit an HTML string to ScreenshotNeo’s URL screenshot endpoint?

The shown ScreenshotNeo call takes a URL. The DIY endpoint in this guide is the option for rendering raw HTML submitted in a POST request.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.