# HTML to Image API (html2img.com) — Full Reference > REST API that converts HTML, CSS, or any URL into a PNG image. Authenticate with one header, POST JSON, receive a CDN-hosted PNG URL. Free tier with 25 renders/month (no credit card). Paid plans from $9/month for 1,000 renders up to $300/month for 100,000. This file bundles the full documentation for LLMs and other automated readers. The shorter index is at [/llms.txt](https://html2img.com/llms.txt). The site sitemap is at [/sitemap.xml](https://html2img.com/sitemap.xml). --- ## What you can do with html2img.com - **Open Graph and social share images** generated on demand (Twitter, LinkedIn, Facebook, Instagram, Pinterest, YouTube thumbnails). - **Invoices, receipts, tickets and certificates** as PNG attachments for transactional email. - **Screenshots of any public URL**, full-page or cropped to a CSS selector, with cookie banners stripped via injected CSS. - **Code screenshots, GitHub repo cards, GitHub social previews** for launches, release notes and changelogs. - **Charts, dashboards and embed cards** exported to PNG with full JavaScript support up to a 30-second budget. - **Real estate listings, product cards, weather widgets, QR codes** and other parameterized image shapes via named templates. Anywhere your stack already speaks HTTP, you can render an image — PHP, Laravel, Rails, Python, Node.js, Express, Next.js, Remix, Vue, Nuxt, FastAPI, Flask, Django. ## Pricing Free: 25 credits/month, no credit card, every feature available. - $9/month: 1,000 credits - $25/month: 3,000 credits - $60/month: 10,000 credits - $120/month: 30,000 credits - $225/month: 65,000 credits - $300/month: 100,000 credits One credit = one render. Overage returns HTTP 429 with the next reset date — no surprise billing. No per-seat pricing. Cancel from the dashboard at any time. Yearly invoicing available with a small discount. Non-profit and open-source discounts considered case by case. [Pricing page](https://html2img.com/pricing) · [Compare to alternatives](https://html2img.com/compare) ## Frequently asked **Free tier?** 25 renders/month, no credit card required. Every feature on paid plans is available on the free plan, including templates, webhooks and DPI control. **Which languages?** Any language that can make an HTTP request. Worked examples published for PHP, Laravel, Ruby on Rails, Python, JavaScript, Node.js, React and Vue. **HTML vs Screenshot endpoint?** HTML accepts raw markup and runs your inline JavaScript. Screenshot accepts a URL and captures the rendered page without running user-supplied JavaScript. Pick HTML for full control, Screenshot for capturing existing pages. **Rendering accuracy?** Real Chrome — flexbox, grid, custom properties, custom fonts and JavaScript all behave as they do in the browser. **What's a template?** A named, parameterized image design hosted on our side. POST a JSON payload to its endpoint, receive a styled PNG, skip the HTML step. **Limits?** 30-second budget on sync requests. For longer renders use `webhook_url`. Viewport up to 5000x5000. DPI 1–4 (forced to 1 when `fullpage` is true). --- ## The three endpoints Base URL: `https://app.html2img.com` ### 1. HTML and CSS API — `POST /api/html` Send raw HTML and CSS. We render it in Chrome and return a PNG URL. Inline `", "wait_for_selector": "#chart canvas" } ``` ### Screenshot endpoint Cannot run user-supplied JavaScript. The target page runs its own scripts; we capture the result. Use `wait_for_selector` for elements that load late, or `ms_delay` for iframes/embeds where the selector isn't visible from the outer document. --- ## Testing and debugging ### Reproducing renders locally 1. Save HTML to a file and open in Chrome. 2. DevTools → device toolbar → set width/height to match what you'll send. 3. Verify fonts load. If Inter falls back to Times locally, it'll fall back the same way in the API. 4. Use Chrome's "Capture screenshot" in DevTools to compare against what the API produces. ### Why a render differs from your browser 1. **Missing fonts** — use Google Fonts via `` or self-host with a public `@font-face` URL. 2. **JS timing** — pair `wait_for_selector` or `ms_delay` with the marker your code sets after data is ready. 3. **User-agent–dependent CSS** — hover, focus, prefers-color-scheme behave differently in headless. Inject `prefers-color-scheme: dark` overrides via `css` if needed. 4. **Slow third-party assets** — strip them with `css { display: none !important }` for ad slots and analytics overlays. ### Webhook testing in development - [webhook.site](https://webhook.site) for a free temporary URL. - ngrok (`ngrok http 3000`) to expose your local server with a stable HTTPS URL. --- ## Language guides Every example assumes the API key lives in `HTML2IMG_API_KEY` (or your platform's equivalent env var). All examples return the same JSON shape (`success`, `credits_remaining`, `id`, `url`). ### PHP (cURL) ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-API-Key: ' . getenv('HTML2IMG_API_KEY'), ], CURLOPT_POSTFIELDS => json_encode(array_merge(['html' => $html], $options)), ]); $response = curl_exec($ch); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $result = json_decode($response, true); if ($statusCode !== 200 || empty($result['success'])) { throw new Exception($result['message'] ?? 'Render failed'); } return $result; } $result = htmlToImage('

Hello

', ['width' => 1200, 'height' => 630]); echo $result['url']; ``` ### PHP (Guzzle service class) ```php namespace App\Services; use GuzzleHttp\Client; class Html2ImgClient { private Client $client; public function __construct(string $apiKey) { $this->client = new Client([ 'base_uri' => 'https://app.html2img.com/api/', 'headers' => ['X-API-Key' => $apiKey, 'Content-Type' => 'application/json'], ]); } public function renderHtml(string $html, array $options = []): string { $response = $this->client->post('html', ['json' => array_merge(['html' => $html], $options)]); return json_decode($response->getBody()->getContents(), true)['url']; } public function screenshot(string $url, array $options = []): string { $response = $this->client->post('screenshot', ['json' => array_merge(['url' => $url], $options)]); return json_decode($response->getBody()->getContents(), true)['url']; } } ``` ### Laravel (Http facade) ```php use Illuminate\Support\Facades\Http; $response = Http::withHeaders(['X-API-Key' => config('services.html2img.key')]) ->post('https://app.html2img.com/api/html', [ 'html' => view('emails.invoice', ['order' => $order])->render(), 'width' => 800, 'height' => 600, ]); $url = $response->json('url'); ``` ### Laravel (queueable job) ```php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; use App\Models\Invoice; class RenderInvoiceImage implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct(public Invoice $invoice) {} public function handle(): void { $response = Http::withHeaders(['X-API-Key' => config('services.html2img.key')]) ->post('https://app.html2img.com/api/v1/templates/invoice-image', [ 'invoice_number' => $this->invoice->number, 'business_name' => $this->invoice->business->name, 'client_name' => $this->invoice->client->name, 'items' => $this->invoice->items->map->toApiArray()->all(), 'total' => $this->invoice->total_formatted, ]); $this->invoice->update(['image_url' => $response->json('url')]); } } RenderInvoiceImage::dispatch($invoice); ``` ### Ruby on Rails (Net::HTTP) ```ruby require 'net/http' require 'json' uri = URI('https://app.html2img.com/api/html') request = Net::HTTP::Post.new(uri) request['Content-Type'] = 'application/json' request['X-API-Key'] = Rails.application.credentials.html2img[:api_key] request.body = { html: render_to_string(template: 'invoices/template', layout: false, locals: { order: order }), width: 800, height: 600 }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } url = JSON.parse(response.body)['url'] ``` ### Ruby on Rails (Faraday with retries) ```ruby require 'faraday' require 'faraday/retry' class Html2ImgClient def initialize(api_key) @conn = Faraday.new(url: 'https://app.html2img.com/api/') do |f| f.request :retry, max: 3, interval: 0.5, backoff_factor: 2, exceptions: [Faraday::ConnectionFailed, Faraday::TimeoutError] f.request :json f.response :json f.headers['X-API-Key'] = api_key f.adapter Faraday.default_adapter end end def render_html(html, options = {}) @conn.post('html', { html: html }.merge(options)).body['url'] end def render_template(slug, payload) @conn.post("v1/templates/#{slug}", payload).body['url'] end end ``` ActiveStorage attach pattern: ```ruby require 'open-uri' URI.open(api_url) do |image| order.invoice_image.attach(io: image, filename: "invoice-#{order.id}.png", content_type: 'image/png') end ``` ### Python (requests) ```python import os, requests response = requests.post( 'https://app.html2img.com/api/html', json={'html': '

Hello

', 'width': 1200, 'height': 630}, headers={'X-API-Key': os.environ['HTML2IMG_API_KEY']}, ) response.raise_for_status() url = response.json()['url'] ``` ### Python (async httpx) ```python import asyncio, os, httpx async def render_html(html: str, options: dict | None = None) -> str: async with httpx.AsyncClient(timeout=35.0) as client: response = await client.post( 'https://app.html2img.com/api/html', headers={'X-API-Key': os.environ['HTML2IMG_API_KEY']}, json={'html': html, **(options or {})}, ) response.raise_for_status() return response.json()['url'] asyncio.run(render_html('

Hello

', {'width': 1200, 'height': 630})) ``` ### Flask ```python from flask import Flask, jsonify, redirect import os, requests app = Flask(__name__) @app.route('/screenshot') def screenshot(): response = requests.post( 'https://app.html2img.com/api/screenshot', json={'url': 'https://example.com', 'fullpage': True}, headers={'X-API-Key': os.environ['HTML2IMG_API_KEY']}, ) data = response.json() return redirect(data['url']) if data.get('success') else (jsonify(data), 500) ``` ### FastAPI ```python from fastapi import FastAPI, HTTPException from fastapi.responses import RedirectResponse import os, requests app = FastAPI() @app.get('/og') async def og(title: str): response = requests.post( 'https://app.html2img.com/api/html', json={'html': f'

{title}

', 'width': 1200, 'height': 630}, headers={'X-API-Key': os.environ['HTML2IMG_API_KEY']}, ) data = response.json() if not data.get('success'): raise HTTPException(status_code=500, detail=data.get('message')) return RedirectResponse(data['url']) ``` ### Node.js (native fetch, 18+) ```javascript const response = await fetch('https://app.html2img.com/api/html', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.HTML2IMG_API_KEY }, body: JSON.stringify({ html: '

Hello

', width: 1200, height: 630 }) }); const { url } = await response.json(); ``` ### Node.js (fetch with retry) ```javascript async function renderHtmlWithRetry(html, options = {}, attempts = 3) { for (let attempt = 0; attempt < attempts; attempt++) { const response = await fetch('https://app.html2img.com/api/html', { method: 'POST', headers: { 'X-API-Key': process.env.HTML2IMG_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ html, ...options }), }); if (response.ok) return (await response.json()).url; if (response.status >= 500 && attempt < attempts - 1) { await new Promise((r) => setTimeout(r, 500 * (attempt + 1))); continue; } throw new Error(`html2img returned ${response.status}`); } } ``` ### Express.js ```javascript import express from 'express'; const app = express(); app.get('/screenshot', async (req, res) => { const response = await fetch('https://app.html2img.com/api/screenshot', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.HTML2IMG_API_KEY }, body: JSON.stringify({ url: req.query.url ?? 'https://example.com', fullpage: true }) }); const data = await response.json(); data.success ? res.redirect(data.url) : res.status(500).json({ error: data.message }); }); app.listen(3000); ``` ### React (custom hook, proxied through your API route) ```jsx import { useCallback, useEffect, useState } from 'react'; export function useShareImage(html, options = {}) { const [url, setUrl] = useState(null); const [error, setError] = useState(null); const render = useCallback(async (signal) => { try { const response = await fetch('/api/share-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html, ...options }), signal, }); if (!response.ok) throw new Error(`Status ${response.status}`); const data = await response.json(); setUrl(data.url); } catch (err) { if (err.name !== 'AbortError') setError(err); } }, [html, options]); useEffect(() => { const controller = new AbortController(); render(controller.signal); return () => controller.abort(); }, [render]); return { url, error }; } ``` Next.js Route Handler (proxy to keep the key server-side): ```typescript // app/api/render-invoice/route.ts export async function POST(request: Request) { const body = await request.json(); const response = await fetch('https://app.html2img.com/api/v1/templates/invoice-image', { method: 'POST', headers: { 'X-API-Key': process.env.HTML2IMG_API_KEY!, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const { url } = await response.json(); return Response.json({ url }); } ``` ### Vue 3 (Composition API) ```vue ``` ### Nuxt server route ```typescript // server/api/render-invoice.post.ts export default defineEventHandler(async (event) => { const body = await readBody(event); const response = await $fetch('https://app.html2img.com/api/v1/templates/invoice-image', { method: 'POST', headers: { 'X-API-Key': process.env.HTML2IMG_API_KEY!, 'Content-Type': 'application/json' }, body, }); return { url: response.url }; }); ``` --- ## Named templates (JSON in, PNG out) Templates skip the markup step entirely. POST a JSON payload to `/api/v1/templates/{slug}` and receive a styled PNG. The full catalog is at [/templates](https://html2img.com/templates). Example — invoice template: ```bash curl -X POST https://app.html2img.com/api/v1/templates/invoice-image \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "invoice_number": "INV-2026-0042", "business_name": "Coastline Coffee Co", "client_name": "Riverside Bakery", "items": [ {"description": "Wholesale beans", "quantity": "50", "unit_price": "$15.00", "amount": "$750.00"} ], "total": "$750.00" }' ``` Template categories: - **Social**: open-graph-image, twitter-post, instagram-square, instagram-story, linkedin-post, facebook-post, pinterest-pin, youtube-thumbnail, tweet-mockup-card. - **Business**: invoice-image, receipt-image, event-ticket, certificate-of-completion, product-card, business-card, coupon-voucher. - **Developer**: code-screenshot, github-repo-card, github-social-preview, project-showcase. - **Real estate**: real-estate-listing. - **Content**: quote-card, blog-hero, podcast-cover, podcast-episode-card, email-header. Each template has its own JSON schema documented at `/templates/{slug}`. --- ## Worked examples The site publishes worked examples for common image shapes. Each links to a full code walkthrough. - [Twitter/X embed to image](https://html2img.com/docs/examples/twitter-embed) — capture a tweet permalink with cookie banners stripped. - [Facebook post to image](https://html2img.com/docs/examples/facebook-post) — handle Facebook SDK timing with `ms_delay`. - [Instagram post to image](https://html2img.com/docs/examples/instagram-post) — size an Instagram embed for capture. - [Invoice or receipt to image](https://html2img.com/docs/examples/invoice-receipt) — render a styled invoice as an A4 PNG. - [GitHub repo card](https://html2img.com/docs/examples/github-repo-card) — social preview for a repository. - [Chart screenshot](https://html2img.com/docs/examples/chart-screenshot) — turn Chart.js into a static PNG. - [Product card](https://html2img.com/docs/examples/product-card) — e-commerce social share image. - [Testimonial card](https://html2img.com/docs/examples/testimonial-card) — quote, avatar, rating. - [Price comparison](https://html2img.com/docs/examples/price-comparison) — pricing table as a marketing PNG. - [Calendar event](https://html2img.com/docs/examples/calendar-event) — date badge, location, attendee list. - [Weather widget](https://html2img.com/docs/examples/weather-widget) — current conditions and forecast. - [QR code](https://html2img.com/docs/examples/qr-code) — branded QR with logo and label. --- ## Free in-browser tools No-signup tools at [/tools](https://html2img.com/tools) for generating Open Graph cards, Twitter cards, code screenshots, YouTube thumbnails, certificates, invoices and Pinterest pins directly in the browser. Built on the same API. --- ## Comparisons Honest comparisons against the most common alternatives: - [vs HTML/CSS to Image (htmlcsstoimage.com)](https://html2img.com/compare/htmlcsstoimage) - [vs Urlbox](https://html2img.com/compare/urlbox) - [vs APIFlash](https://html2img.com/compare/apiflash) - [vs Bannerbear](https://html2img.com/compare/bannerbear) Each covers pricing, features and migration effort. --- ## Reference URLs - Site: https://html2img.com - API base: https://app.html2img.com - CDN: https://i.html2img.com - Sign up: https://app.html2img.com/register - Dashboard: https://app.html2img.com/dashboard - Docs home: https://html2img.com/docs/getting-started - Parameters: https://html2img.com/docs/parameters - Language guides: https://html2img.com/docs/usage - Worked examples: https://html2img.com/docs/examples - Templates catalog: https://html2img.com/templates - Free tools: https://html2img.com/tools - Pricing: https://html2img.com/pricing - Features: https://html2img.com/features - Articles: https://html2img.com/articles - Contact: https://html2img.com/contact - Sitemap: https://html2img.com/sitemap.xml - Image sitemap: https://html2img.com/image-sitemap.xml