Home / Docs / Usage / HTML to Image in JavaScript and Node.js

HTML to Image in JavaScript and Node.js

Render HTML to a PNG from Node.js using native fetch or axios.

Prefer the official SDK?

Most JavaScript projects should use the official JavaScript SDK, a zero-dependency typed client you install with npm install @html2img/client. This guide shows the underlying HTTP calls for when you want to keep dependencies at zero yourself or understand what the SDK does under the hood.

Best for: Node-based backends, serverless functions on Vercel/Netlify/Cloudflare, and any build script that needs OG images on demand. Native fetch works in Node 18+; axios adds richer interceptors.

Worked examples for the HTML, Screenshot and Templates endpoints, plus a fetch retry pattern.

API Response Format

Every successful request returns a JSON body with a url pointing to the generated image:

{
    "success": true,
    "id": "8a9dda43-5f42-4b93-8ff4-cd69ed32d402",
    "expires_at": null,
    "credits_remaining": 950,
    "url": "https://i.html2img.com/image-1786092598870-921691.png"
}

You can either link to the url directly, or download the image from it if you want a local copy. expires_at is null on paid plans; on the free tier it carries an ISO 8601 timestamp - free-tier renders expire after 7 days, and upgrading makes existing renders permanent.

Node.js Examples

HTML to Image Example

Here’s a simple example using the built-in fetch (Node.js 18+):

import fs from 'fs/promises';

async function htmlToImage(html, css = '') {
  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,
      css,
      width: 800,
      height: 600
    })
  });

  const data = await response.json();

  if (!response.ok || !data.success) {
    throw new Error(data.message || 'Failed to generate image');
  }

  console.log(`Image available at: ${data.url}`);
  console.log(`Credits remaining: ${data.credits_remaining}`);

  // Optionally download the image to disk
  const imageResponse = await fetch(data.url);
  const buffer = Buffer.from(await imageResponse.arrayBuffer());
  await fs.writeFile('output.png', buffer);

  return data.url;
}

// Example usage
const html = `
  <div style="padding: 20px; background: #f0f0f0;">
    <h1>Hello from Node.js!</h1>
    <p>Generated at ${new Date().toLocaleString()}</p>
  </div>
`;

const css = `
  h1 { color: #2563eb; }
  p { color: #4b5563; }
`;

htmlToImage(html, css).catch((error) => {
  console.error('Error:', error.message);
});

Screenshot Example

Taking a screenshot of a webpage:

async function takeScreenshot(url) {
  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,
      width: 1200,
      height: 800,
      dpi: 2,
      fullpage: true
    })
  });

  const data = await response.json();

  if (!response.ok || !data.success) {
    throw new Error(data.message || 'Failed to take screenshot');
  }

  return data.url;
}

takeScreenshot('https://example.com')
  .then((imageUrl) => console.log(`Screenshot: ${imageUrl}`))
  .catch((error) => console.error('Error:', error.message));

Express.js Example

Here’s how to use it with Express.js. Since the API returns a URL, you can simply redirect (or return JSON) rather than proxying the image bytes:

import express from 'express';

const app = express();

app.get('/generate-image', async (req, res) => {
  try {
    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: `
          <div style="padding: 20px; background: #f0f0f0;">
            <h1>Hello from Express!</h1>
            <p>Generated at ${new Date().toLocaleString()}</p>
          </div>
        `,
        width: 800,
        height: 600
      })
    });

    const data = await response.json();

    if (!data.success) {
      throw new Error(data.message || 'Failed to generate image');
    }

    res.json({ url: data.url, id: data.id });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/screenshot', async (req, res) => {
  try {
    const { url = 'https://example.com' } = req.query;

    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,
        width: 1200,
        height: 800,
        dpi: 2,
        fullpage: true
      })
    });

    const data = await response.json();

    if (!data.success) {
      throw new Error(data.message || 'Failed to take screenshot');
    }

    res.redirect(data.url);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Browser Examples

Using Fetch API

Here’s how to use the API directly in the browser. Because the response is JSON containing a public image URL, you can use it as an <img> src directly - no blob handling required:

async function generateImage() {
  try {
    const response = await fetch('https://app.html2img.com/api/html', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': 'your-api-key' // Be careful with API key exposure!
      },
      body: JSON.stringify({
        html: `
          <div style="padding: 20px; background: #f0f0f0;">
            <h1>Hello from the Browser!</h1>
            <p>Generated at ${new Date().toLocaleString()}</p>
          </div>
        `,
        width: 800,
        height: 600
      })
    });

    const data = await response.json();

    if (!data.success) {
      throw new Error(data.message || 'Failed to generate image');
    }

    // Display the image by pointing an <img> at the returned URL
    const img = document.createElement('img');
    img.src = data.url;
    document.body.appendChild(img);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

React Component Example

Here’s a React component that uses the API:

import { useState } from 'react';

function ImageGenerator() {
  const [imageUrl, setImageUrl] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const generateImage = async () => {
    setLoading(true);
    setError(null);

    try {
      // Local demo only - in production, proxy through your server so the key never ships to the browser
      const response = await fetch('https://app.html2img.com/api/html', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': process.env.REACT_APP_HTML2IMG_API_KEY
        },
        body: JSON.stringify({
          html: `
            <div style="padding: 20px; background: #f0f0f0;">
              <h1>Hello from React!</h1>
              <p>Generated at ${new Date().toLocaleString()}</p>
            </div>
          `,
          width: 800,
          height: 600
        })
      });

      const data = await response.json();

      if (!data.success) {
        throw new Error(data.message || 'Failed to generate image');
      }

      setImageUrl(data.url);
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <button
        onClick={generateImage}
        disabled={loading}
      >
        {loading ? 'Generating...' : 'Generate Image'}
      </button>

      {error && (
        <div style={{ color: 'red' }}>
          Error: {error}
        </div>
      )}

      {imageUrl && (
        <div>
          <img
            src={imageUrl}
            alt="Generated content"
            style={{ maxWidth: '100%' }}
          />
        </div>
      )}
    </div>
  );
}

export default ImageGenerator;

Vue Component Example

Here’s a Vue 3 component that uses the API:

<template>
  <div>
    <button
      @click="generateImage"
      :disabled="loading"
    >
      {{ loading ? 'Generating...' : 'Generate Image' }}
    </button>

    <div v-if="error" class="error">
      Error: {{ error }}
    </div>

    <div v-if="imageUrl">
      <img
        :src="imageUrl"
        alt="Generated content"
        style="max-width: 100%"
      />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const imageUrl = ref(null);
const loading = ref(false);
const error = ref(null);

const generateImage = async () => {
  loading.value = true;
  error.value = null;

  try {
    const response = await fetch('https://app.html2img.com/api/html', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': import.meta.env.VITE_HTML2IMG_API_KEY
      },
      body: JSON.stringify({
        html: `
          <div style="padding: 20px; background: #f0f0f0;">
            <h1>Hello from Vue!</h1>
            <p>Generated at ${new Date().toLocaleString()}</p>
          </div>
        `,
        width: 800,
        height: 600
      })
    });

    const data = await response.json();

    if (!data.success) {
      throw new Error(data.message || 'Failed to generate image');
    }

    imageUrl.value = data.url;
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};
</script>

<style scoped>
.error {
  color: red;
  margin: 1rem 0;
}
</style>

When using the API in the browser, make sure to handle your API key securely. Never expose it directly in client-side code. Instead, proxy your requests through a backend server.

For production apps, add your own request throttling, error handling, and loading states for a smoother experience.

Common patterns

fetch with retry

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;
    }

    // Do not retry 504: a timed-out render may still complete, and each retry can spend a credit
    if (response.status >= 500 && response.status !== 504 && attempt < attempts - 1) {
      await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
      continue;
    }

    throw new Error(`html2img returned ${response.status}`);
  }
}

const url = await renderHtmlWithRetry('<h1>Hello</h1>', { width: 1200, height: 630 });

Backs off on 5xx, and bails on 4xx so you do not retry validation errors. It also bails on 504 - a timed-out render may still complete, and each retry can spend another credit, so for long captures pass webhook_url instead.

Templates in JavaScript

Use a named template when your data is structured. Here is the Invoice Image template:

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({
    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',
  }),
});

if (!response.ok) {
  throw new Error(`html2img returned ${response.status}`);
}

const { url } = await response.json();

The returned url is hosted on i.html2img.com.

Rendering a PDF instead

Request a PDF by adding format: 'pdf' to the body:

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: '<h1>Invoice #1042</h1><p>Due within 30 days.</p>',
    format: 'pdf'
  })
});
const { url } = await response.json(); // ends in .pdf

What changes: pages are A4 portrait, long content paginates automatically and the output is vector with selectable text rather than a bitmap. The format parameter docs cover the details and the HTML to PDF API page has the overview.

Common templates for JavaScript and Node developers

JavaScript and Node apps most often reach for these named templates: