ScreenshotNeo

BlogUse cases

HTML to Image API for Sales Automation

Generate personalized product cards, testimonials, invoices and social images from CRM data with HTML-to-image APIs.

By the ScreenshotNeo team1 October 202610 min read

Direct answer: An HTML-to-image API turns CRM or campaign data into a rendered PNG, JPEG, WebP or PDF. You keep the design in HTML and CSS, fill placeholders with product, pricing or salesperson data, then send the markup or a template URL to a browser-based renderer. The returned asset can be inserted into sales emails, proposals, landing pages, social posts and Open Graph metadata.

For sales automation, use a fixed, versioned template; validate and escape every field; render asynchronously for large batches; store a deterministic content hash; and record the rendering result with the campaign record. This makes retries safe and lets you regenerate an asset when a price, discount or testimonial changes.

What an HTML-to-image API does

The service runs HTML and CSS in a hosted browser, waits for assets and scripts, and captures the rendered page. Depending on the provider, the input can be:

  • Raw HTML and CSS generated by your application.
  • A public webpage URL.
  • A named template plus JSON data.

The output may be a binary image, a hosted image URL or a PDF. Browser rendering matters because the result depends on JavaScript execution, web fonts, external images, CSS support, viewport dimensions and the point at which the page is considered ready.

Sales automation architecture

  1. Collect data. Read fields such as product name, price, discount, testimonial, avatar URL and salesperson name from your CRM or campaign system.
  2. Select a template. Keep the HTML and CSS in source control and give each version an identifier.
  3. Render. Send either escaped markup or a URL that serves the template with its data.
  4. Validate. Check the response status, content type, dimensions and a page verdict when the provider supplies one.
  5. Store and publish. Save the image with a content hash, then insert it into an email, proposal, landing page, social post or og:image tag.
  6. Retry safely. Use an idempotency key or a hash of template version plus input data so a timeout does not create duplicate assets.

Design a reusable sales template

Keep the template deterministic. Use a fixed canvas, local or allowlisted assets, explicit font stacks and a fallback for missing fields. Do not concatenate untrusted values directly into CSS or markup.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    * { box-sizing: border-box; }
    body {
      margin: 0;
      width: 1200px;
      height: 630px;
      font-family: Inter, Arial, sans-serif;
      background: #f4f7fb;
      color: #10233f;
    }
    .card { padding: 64px; height: 100%; display: flex; flex-direction: column; justify-content: space-between; }
    .eyebrow { color: #5273a7; font-size: 24px; font-weight: 700; }
    h1 { margin: 18px 0 12px; font-size: 64px; line-height: 1.05; }
    .description { max-width: 760px; font-size: 28px; color: #4e607c; }
    .price { font-size: 52px; font-weight: 800; }
    .discount { color: #087443; font-size: 24px; font-weight: 700; }
    .footer { display: flex; justify-content: space-between; align-items: end; }
    .rep { font-size: 22px; color: #4e607c; }
  </style>
</head>
<body>
  <main class="card">
    <section>
      <div class="eyebrow">{{company}}</div>
      <h1>{{product_name}}</h1>
      <div class="description">{{description}}</div>
    </section>
    <section class="footer">
      <div>
        <div class="price">{{price}}</div>
        <div class="discount">{{discount_text}}</div>
      </div>
      <div class="rep">Contact {{salesperson}}</div>
    </section>
  </main>
</body>
</html>

Replace placeholders with an escaping template engine. Keep currency formatting, date formatting and localization in application code so every renderer receives already formatted, display-safe strings.

DIY rendering with a hosted browser

This complete Node.js example uses Playwright to render a product card locally. It is useful when you need full control over the browser process or want to evaluate a provider against your own template.

mkdir sales-card-renderer && cd sales-card-renderer
npm init -y
npm install playwright
npx playwright install chromium
// render.mjs
import { chromium } from 'playwright';
import { writeFile } from 'node:fs/promises';

const escapeHtml = (value) => String(value)
  .replaceAll('&', '&amp;')
  .replaceAll('<', '&lt;')
  .replaceAll('>', '&gt;')
  .replaceAll('"', '&quot;')
  .replaceAll("'", '&#39;');

const data = {
  company: 'Northwind Tools',
  productName: 'Team Analytics',
  description: 'See pipeline health and forecast risk in one place.',
  price: '$49 / user / month',
  discountText: 'Save 20% through June 30',
  salesperson: 'Maya Chen'
};

const html = `<!doctype html>
<html><head><meta charset="utf-8">
<style>
*{box-sizing:border-box}body{margin:0;width:1200px;height:630px;font-family:Arial,sans-serif;background:#f4f7fb;color:#10233f}.card{padding:64px;height:100%;display:flex;flex-direction:column;justify-content:space-between}.eyebrow{color:#5273a7;font-size:24px;font-weight:700}h1{margin:18px 0 12px;font-size:64px;line-height:1.05}.description{max-width:760px;font-size:28px;color:#4e607c}.price{font-size:52px;font-weight:800}.discount{color:#087443;font-size:24px;font-weight:700}.footer{display:flex;justify-content:space-between;align-items:end}.rep{font-size:22px;color:#4e607c}
</style></head>
<body><main class="card">
<section><div class="eyebrow">${escapeHtml(data.company)}</div>
<h1>${escapeHtml(data.productName)}</h1>
<div class="description">${escapeHtml(data.description)}</div></section>
<section class="footer"><div><div class="price">${escapeHtml(data.price)}</div>
<div class="discount">${escapeHtml(data.discountText)}</div></div>
<div class="rep">Contact ${escapeHtml(data.salesperson)}</div></section>
</main></body></html>`;

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 });
await page.setContent(html, { waitUntil: 'networkidle' });
await page.screenshot({ path: 'sales-card.png', type: 'png' });
await writeFile('sales-card.html', html);
await browser.close();
node render.mjs

For remote fonts or images, wait for a specific selector or for fonts to finish loading instead of relying only on a short delay:

await page.waitForSelector('.card');
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'sales-card.webp', type: 'webp', quality: 88 });

Choosing API inputs and output options

Decision Use this when Watch for
Raw HTML/CSS Your application owns the complete layout. Escape data and control external resources.
Public URL A deployed template already contains the data. Authentication, cache invalidation and URL exposure.
Named template plus data Many campaigns share one design. Template versioning and schema validation.
PNG Lossless text, diagrams or transparency. Larger files.
JPEG Photographic cards where small files matter. No transparency and possible text artifacts.
WebP Modern web delivery and smaller files. Check recipient support before email use.
PDF Quotes, invoices, proposals or print workflows. Page size, margins, fonts and page breaks.

Compare providers on browser fidelity, JavaScript and font support, external asset handling, template reuse, webhook callbacks, batch limits, authentication, URL access controls, custom headers, retention, rate limits, credits and error behavior. These operational details change, so verify current vendor documentation before committing.

Calling an API from your application

The examples below use ScreenshotNeo for a URL-backed template. The ScreenshotNeo API documentation lists its request options. The same integration pattern applies to an HTML-to-image provider that accepts raw markup or named templates.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Node.js

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(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await Bun.write('shot.webp', image);

For a campaign, generate a unique template URL containing a signed campaign identifier rather than putting sensitive CRM data in a query string. If your provider accepts HTML directly, send the escaped template body and the same rendering controls as query or JSON fields.

Automating campaign assets

Product and offer cards

Render one card per SKU or offer, including a stable product identifier in the filename. Regenerate only when the template version, price, inventory message or offer expiry changes.

Testimonials

Validate testimonial length and image dimensions before rendering. Truncate or resize deliberately so one unusually long quote cannot change the card layout.

Invoices and proposals

Use PDF output when recipients need to print or archive the document. Set paper size, margins, landscape mode and page ranges explicitly, and test long line items and page breaks.

Open Graph images

Generate one image per product or CMS page and put its stable URL in og:image. Cache by page revision so social crawlers do not receive a different image for the same URL.

Email and landing pages

Host the final image at a URL that remains available for the lifetime of the campaign. Add meaningful alt text and a text version of the offer because images can be blocked.

Batching, webhooks and idempotency

Synchronous calls are convenient for a single card. For a campaign, queue jobs and use a webhook callback where the provider supports one. Store the job ID, template version, input hash and output URL. Verify webhook signatures when available, make the handler idempotent, and return a success response quickly before doing storage or email work.

  1. Write a queued record before submitting the render.
  2. Use a key such as sha256(template_version + canonical_json(data) + output_options).
  3. Retry transient network and 5xx failures with exponential backoff and a limit.
  4. Send permanent validation or authentication failures to a dead-letter queue.
  5. Reconcile jobs periodically so a lost webhook cannot leave an asset stuck forever.

Security and privacy checklist

  • Keep API keys in a secret manager and never ship them to browser JavaScript.
  • Escape text and attribute values; reject untrusted HTML, CSS and script unless the renderer is isolated for that purpose.
  • Allowlist remote image and font hosts to reduce SSRF and supply-chain risk.
  • Do not place customer names, prices or tokens in publicly guessable URLs.
  • Remove sensitive fields from logs and set a retention policy for source HTML and rendered files.
  • Use custom headers or cookies only when the provider documents how they are protected.

Troubleshooting

Symptom Likely cause Fix
Blank image The page was captured before content rendered or a script failed. Wait for a selector or network idle, inspect browser logs, and provide fallbacks for failed data.
Missing fonts The font request was blocked, slow or not loaded before capture. Use an approved font host, wait for document.fonts.ready, and define fallback fonts.
Images missing Relative URLs, authentication or cross-origin restrictions. Use absolute URLs, provide the required headers or cookies, and verify the asset independently.
Text is clipped Unexpectedly long CRM data changed layout. Constrain lengths, wrap intentionally, or calculate font size and height from data.
Different results between runs Animations, current time, random content or changing remote assets. Freeze timestamps, disable animation, pin asset versions and wait for a deterministic readiness signal.
401 or 403 Missing or incorrect API credentials. Check the authentication method, environment variable and account permissions; do not retry unchanged credentials.
429 Rate or concurrency limit. Queue work, honor Retry-After, add exponential backoff and reduce parallelism.
Timeout Slow page, blocked request or excessive JavaScript. Set a realistic timeout, block unnecessary resources, simplify the template and capture asynchronously.
Social preview is stale Crawler or CDN cache. Use a versioned image URL and keep the old asset available during propagation.

Performance, reliability and cost

  • Reduce render time: reuse templates, inline critical CSS, limit third-party requests, optimize images and avoid unnecessary JavaScript.
  • Control concurrency: match workers to provider limits and your own queue capacity. More parallel requests can increase throttling and browser memory use.
  • Cache deliberately: key results by normalized data, template version and output settings. Set an expiration when prices or inventory can change.
  • Measure useful signals: record queue time, render time, response status, output bytes, retry count and the provider’s billing or page verdict headers.
  • Budget for failures: distinguish validation errors from transient failures and do not charge a customer twice for a retry.
  • Estimate cost: multiply expected assets by the provider’s billable-render rules, then add storage, delivery and any PDF or batch charges. Published plan limits and prices can change, so verify them before launch.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. Point one GET request at a hosted HTML/CSS template and receive a PNG, JPEG, WebP or PDF. Cookie banners, newsletter popups and chat widgets are removed before the shot; bot checks, blank pages and failed loads are never billed. Each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP tools let Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Every plan includes the available features, including full-page and element capture, device presets, retina scale, custom CSS and JavaScript, waiting controls, request blocking, headers and cookies, timezone and geolocation, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture and a usage API. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Should I generate the image when the lead is created or when the email is sent?

Generate ahead of delivery when the design is stable and the campaign is large. Generate at send time when price, inventory or personalization must be current.

Can the same template produce both social cards and email graphics?

Yes, if you parameterize dimensions and keep text within safe bounds. In practice, separate layouts often produce better results for each channel.

When is PDF preferable to an image?

Use PDF for documents that recipients print, sign or archive. Use an image for inline email, social previews and compact product cards.

How do I prevent a retry from creating duplicate assets?

Persist a deterministic input hash or idempotency key before submitting the job and return the existing result when that key already succeeded.

Do I need a browser automation library if I use an API?

No. A hosted API handles browser installation and execution. A local library such as Playwright is useful when you need direct browser control or an offline development loop.