Tutorial

Convert URL to Image: Best Methods for 2026

April 202610 min readNode.js · Python · REST API

Every way to turn a URL into a PNG or JPEG image — self-hosted with Playwright/Puppeteer, or via a REST API that handles the browser for you. Full code in Node.js and Python.

The Quickest Way (REST API)

If you just need a URL→image and don't want to manage headless browsers, a single HTTP call does it:

// Node.js — URL to PNG in 4 lines
const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
  method: 'POST',
  headers: { 'X-Api-Key': 'YOUR_KEY', 'Content-Type': 'application/json' },
  body: JSON.stringify({ url: 'https://example.com', full_page: true })
});
const { screenshot } = await res.json();
require('fs').writeFileSync('out.png', Buffer.from(screenshot, 'base64'));
# Python — URL to PNG in 4 lines
import requests, base64
r = requests.post('https://api.snapapi.pics/v1/screenshot',
    headers={'X-Api-Key': 'YOUR_KEY'},
    json={'url': 'https://example.com', 'full_page': True})
open('out.png', 'wb').write(base64.b64decode(r.json()['screenshot']))

The rest of this post covers self-hosted options for when you need full control.

Playwright (Node.js)

const { chromium } = require('playwright');
const fs = require('fs');

async function urlToImage(url, outputPath, options = {}) {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage({
    viewport: { width: options.width || 1280, height: options.height || 800 }
  });

  // Disable animations for clean capture
  await page.addStyleTag({ content: '*, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }' });

  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });

  const screenshot = await page.screenshot({
    fullPage: options.fullPage !== false,
    type: options.format || 'png',
    quality: options.format === 'jpeg' ? (options.quality || 85) : undefined
  });

  fs.writeFileSync(outputPath, screenshot);
  await browser.close();
  return screenshot;
}

// Usage
urlToImage('https://news.ycombinator.com', 'hn.png');
urlToImage('https://example.com', 'example.jpg', { format: 'jpeg', quality: 90 });

Playwright (Python)

from playwright.sync_api import sync_playwright

def url_to_image(url: str, output_path: str, full_page: bool = True, width: int = 1280) -> bytes:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page(viewport={'width': width, 'height': 800})

        # Remove animations
        page.add_style_tag(content='*, *::before, *::after { animation-duration: 0s !important; }')

        page.goto(url, wait_until='networkidle')
        buf = page.screenshot(full_page=full_page)

        browser.close()

    with open(output_path, 'wb') as f:
        f.write(buf)
    return buf

url_to_image('https://example.com', 'out.png')

Puppeteer (Node.js)

const puppeteer = require('puppeteer');

async function urlToImage(url, outputPath) {
  const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] });
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  await page.goto(url, { waitUntil: 'networkidle2' });

  // Resize to full page height
  const height = await page.evaluate(() => document.body.scrollHeight);
  await page.setViewport({ width: 1280, height });

  await page.screenshot({ path: outputPath, fullPage: true });
  await browser.close();
}

urlToImage('https://example.com', 'output.png');

Output Format Options

FormatTypical sizeUse when
PNG500KB–3MBPixel-perfect, transparency, text clarity
JPEG80KB–500KBPhotos, thumbnails, bandwidth-sensitive
WebP50KB–300KBModern browsers, best size/quality ratio
PDF100KB–2MBPrint, archival, multi-page documents
// SnapAPI — specify output format
const body = {
  url: 'https://example.com',
  full_page: true,
  format: 'webp'   // 'png' | 'jpeg' | 'webp'
};

// For JPEG, add quality parameter:
// format: 'jpeg', quality: 85   // 1-100

Handling Single-Page Applications (SPAs)

React, Vue, Next.js, and Angular apps load content via JavaScript after the initial HTML. If you capture too early, you get a blank or partial page.

// Playwright — wait for SPA to finish rendering
await page.goto(url, { waitUntil: 'networkidle' });

// Or wait for a specific element that only appears after data loads
await page.waitForSelector('[data-loaded="true"]', { timeout: 10000 });

// Or wait for a specific network request to complete
await page.waitForResponse(res => res.url().includes('/api/data') && res.status() === 200);
const screenshot = await page.screenshot({ fullPage: true });
// SnapAPI — networkidle handles SPAs automatically
const body = {
  url: 'https://react-app.example.com',
  wait_for: 'networkidle',  // Waits for all XHR/fetch to finish
  full_page: true
};

Bypassing Cloudflare and Bot Detection

Many sites use Cloudflare, Akamai Bot Manager, or reCAPTCHA. A plain headless browser will get a 403 or CAPTCHA page instead of the actual content.

# Playwright — basic stealth (not foolproof)
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(
        headless=True,
        args=[
            '--disable-blink-features=AutomationControlled',
            '--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
        ]
    )
    context = browser.new_context(
        user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
    )
    page = context.new_page()
    # Remove webdriver flag
    page.add_init_script('delete Object.getPrototypeOf(navigator).webDriver')
    page.goto('https://cloudflare-protected-site.com', wait_until='networkidle')
    page.screenshot(path='out.png', full_page=True)
    browser.close()
Note: DIY stealth is a constant arms race. Cloudflare updates its detection regularly. SnapAPI's stealth mode is maintained and updated — use "stealth": true in the API request for consistently bypassed captures.

URL-to-Image API Endpoint (Express)

// GET /screenshot?url=https://example.com
const express = require('express');
const app = express();

app.get('/screenshot', async (req, res) => {
  const { url, format = 'png', fullPage = 'true' } = req.query;
  if (!url) return res.status(400).json({ error: 'url required' });

  const response = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, format, full_page: fullPage === 'true', wait_for: 'networkidle' })
  });

  const data = await response.json();
  const buf = Buffer.from(data.screenshot, 'base64');

  res.set('Content-Type', `image/${format}`);
  res.set('Content-Disposition', `inline; filename="screenshot.${format}"`);
  res.set('Cache-Control', 'public, max-age=3600');
  res.send(buf);
});

app.listen(3000, () => console.log('Screenshot service on :3000'));
// Usage: GET http://localhost:3000/screenshot?url=https://example.com

Bulk URL-to-Image Conversion

import asyncio, aiohttp, base64, os

SNAPAPI_KEY = os.environ['SNAPAPI_KEY']
URLS = [
    'https://example.com',
    'https://news.ycombinator.com',
    'https://github.com/trending',
]

async def capture(session: aiohttp.ClientSession, url: str, idx: int) -> None:
    async with session.post(
        'https://api.snapapi.pics/v1/screenshot',
        headers={'X-Api-Key': SNAPAPI_KEY},
        json={'url': url, 'full_page': True, 'wait_for': 'networkidle'}
    ) as resp:
        data = await resp.json()
        with open(f'output_{idx:03d}.png', 'wb') as f:
            f.write(base64.b64decode(data['screenshot']))
        print(f'✅ {url}')

async def bulk_convert(urls: list[str], concurrency: int = 5) -> None:
    semaphore = asyncio.Semaphore(concurrency)
    async with aiohttp.ClientSession() as session:
        async def bounded(url, idx):
            async with semaphore:
                return await capture(session, url, idx)
        await asyncio.gather(*[bounded(u, i) for i, u in enumerate(urls)])

asyncio.run(bulk_convert(URLS))
Free to start: SnapAPI gives you 200 free URL-to-image conversions per month. No credit card. Get your key →

Method Comparison

MethodSetupSPA supportAnti-botServerlessCost
Playwright (self)MediumPartial⚠️ trickyFree
Puppeteer (self)MediumPartial⚠️ trickyFree
Selenium (self)Heavy⚠️ manualPartialFree
SnapAPI RESTMinimal✅ stealth$0.0016/call