HTML/CSS to Image vs Bannerbear: Code or Templates?

HTML/CSS to Image vs Bannerbear for OG and social images: HTML input vs template layers, pricing at three volumes, sync limits and request code for both.

By Simon Chiu · Updated

HTML/CSS to Image (HCTI) and Bannerbear both turn data into images over an API, and both get used for Open Graph cards, social posts and email graphics. The difference is who designs the image and in what.

  • HCTI renders HTML and CSS in headless Chrome. Your developers own the design as code, in the same tools they use for the website.
  • Bannerbear renders templates built in its visual editor. A designer owns the layout; your code fills named layers such as title and photo.

Pick HCTI when the image is part of your codebase and a developer will maintain it. Pick Bannerbear when a non-developer must be able to change the design without a deploy, or when you also need video, animation or PDF output from the same templates.

Disclosure: I build RenderScreenshot, which also renders HTML to images. HCTI and Bannerbear are not our products. We do not have paid accounts with either, so this page has no latency numbers for them. It has the method and the script to get those numbers yourself, list prices checked on 2026-10-15, and request code checked against each vendor's documented request shape.

More pairs are in our comparison hub. For a ranked list of generators, read the best OG image generators.

Verdict

You want... HCTI Bannerbear
Send any HTML and CSS Yes, core feature No
A designer edits the layout without code Visual editor available; HTML is the main path Yes, core feature
Screenshot a live URL Yes (url, full_screen, cookie banner blocking) Not in the image API docs we checked
Synchronous response by default Yes No; async by default, sync host with a 10 s limit
Video, animation and multi-step workflows No Yes
Signed URLs that render on first request Signed image URLs Instant URLs, one template each
Output formats PNG, JPG, WebP, PDF (change the URL extension) JPG, PNG, PDF, WebP, AVIF (you list them)
Lower price at 1k, 10k and 100k images a month Yes No

How each API thinks

HCTI is a browser with an API. You send html and css (or a url), it renders in Chrome, and it returns a URL such as https://hcti.io/v1/image/<id>. The URL is permanent while your account is active and is served through a CDN. Add .jpg, .webp or .pdf to the same URL to get another format without a new render request. Saved templates are HTML with Handlebars variables ({{title}}), rendered with POST /v1/image/:template_id.

Bannerbear is a design tool with an API. You build a template in its editor with named layers, then POST the template UID and a list of modifications. The default host queues the job and answers 202 Accepted; you poll or receive a webhook. The sync.api.bannerbear.com host waits for the render, with a 10-second timeout that returns HTTP 408. The current API version in Bannerbear's docs is v5.

That split shows up in maintenance. With HCTI, a design change is a code change: it goes through review, it can be tested, and it can break a build. With Bannerbear, a design change is an edit in a web app: fast, no deploy, and no record in your repository unless you add one.

Request code side by side

Both scripts are Node 22 ESM with no dependencies, and both throw on any error status. We do not have accounts with either vendor, so we ran them against a local mock that enforces each documented auth scheme and required field. They have not run against the live APIs.

HCTI, HTML in:

// HTML/CSS to Image: send your own HTML and CSS, get back a hosted image URL.
const { HCTI_USER_ID: user, HCTI_API_KEY: key } = process.env;
if (!user || !key) throw new Error('Set HCTI_USER_ID and HCTI_API_KEY');

const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) =>
  ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);

const title = 'Q3 release notes: <faster> & smaller';   // user input: escape it
const res = await fetch('https://hcti.io/v1/image', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Basic ' + Buffer.from(`${user}:${key}`).toString('base64'),
  },
  body: JSON.stringify({
    html: `<div class="card"><h1>${escapeHtml(title)}</h1><p>acme.dev/blog</p></div>`,
    css: `.card{width:1200px;height:630px;box-sizing:border-box;padding:72px;display:flex;
          flex-direction:column;justify-content:center;background:#0f172a;color:#f8fafc;font-family:Inter}
          h1{font-size:64px;margin:0}`,
    google_fonts: 'Inter',
    viewport_width: 1200,
    viewport_height: 630,
  }),
});
if (!res.ok) throw new Error(`HCTI ${res.status}: ${await res.text()}`);
const { url } = await res.json();
console.log('image URL:', url);   // append .png, .jpg or .webp to pick a format

Bannerbear, layers in:

// Bannerbear: fill the layers of a template you designed in their editor.
// The sync host waits for the render (10 second limit); the default host is async.
const key = process.env.BANNERBEAR_API_KEY;
if (!key) throw new Error('Set BANNERBEAR_API_KEY');

const res = await fetch('https://sync.api.bannerbear.com/v5/images', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    template: process.env.BANNERBEAR_TEMPLATE,   // template UID from the dashboard
    formats: ['png'],
    modifications: {
      objects: [
        { name: 'title', text: 'Q3 release notes: <faster> & smaller' },  // plain text, no HTML
        { name: 'photo', image_url: 'https://example.com/cover.jpg' },
      ],
    },
  }),
});
if (!res.ok) throw new Error(`Bannerbear ${res.status}: ${await res.text()}`);
const image = await res.json();
console.log(image.status, image.files.png);

The input-safety difference

Look at the title in both scripts. In HCTI the title becomes HTML, so a user who types <img src=x onerror=...> puts markup into your render. Always escape user text, as the escapeHtml helper does. In Bannerbear the title is a text field, so angle brackets are just characters.

Bannerbear has its own trap instead. Its text layers support a secondary style for text wrapped in *asterisks*. A product name or user comment that contains asterisks can switch font or color in the middle of a line. If your text comes from users, strip or replace asterisks, and test how your template treats them before you go live.

Pricing at three volumes

From each pricing page, checked 2026-10-15. Each cell is the cheapest monthly plan that covers the volume, for one PNG per image at scale 1.

Images per month HCTI Bannerbear
1,000 $14 (Basic, 1,000) $49 (1,000 API credits)
10,000 $69 (Basic, 10,000) $149 (10,000 API credits)
100,000 $375 (Pro, 100,000) $458 (100,000 API credits)
Free tier 50 images a month, no card Trial with 30 API credits, no card

Two billing rules change the Bannerbear column:

  • Formats and scale multiply credits. Bannerbear's FAQ says a JPG at scale 1 is one credit, and JPG plus PNG at scale 2 is four. Ask only for the formats you serve.
  • AI features cost extra credits per layer that uses them (background removal, subject detection).

HCTI lists optional overages at $10 per 1,000 images on Basic and Pro. Overages are off until you turn them on.

How to benchmark the two yourself

Speed matters for OG images rendered on first request, so measure it with your own template. The script below renders ten cards per vendor with unique titles (so nothing comes from cache), downloads each image, and prints the success count and median time. Create the same card in both products first: an HCTI HTML card and a Bannerbear template with a title layer.

// Time HCTI and Bannerbear on the same card: 10 renders each, unique titles so no cache hits.
const hctiAuth = 'Basic ' + Buffer.from(`${process.env.HCTI_USER_ID}:${process.env.HCTI_API_KEY}`).toString('base64');
const median = (a) => [...a].sort((x, y) => x - y)[Math.floor(a.length / 2)];

const providers = {
  hcti: (title) => fetch('https://hcti.io/v1/image', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: hctiAuth },
    body: JSON.stringify({ html: `<div class="c"><h1>${title}</h1></div>`, css: process.env.CARD_CSS ?? '', viewport_width: 1200, viewport_height: 630 }),
  }).then(async (r) => { if (!r.ok) throw new Error(`hcti ${r.status}`); return (await r.json()).url + '.png'; }),
  bannerbear: (title) => fetch('https://sync.api.bannerbear.com/v5/images', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.BANNERBEAR_API_KEY}` },
    body: JSON.stringify({ template: process.env.BANNERBEAR_TEMPLATE, formats: ['png'], modifications: { objects: [{ name: 'title', text: title }] } }),
  }).then(async (r) => { if (!r.ok) throw new Error(`bannerbear ${r.status}`); return (await r.json()).files.png; }),
};

for (const [name, render] of Object.entries(providers)) {
  const times = []; let failures = 0;
  for (let i = 0; i < 10; i++) {
    const title = `Release ${Date.now()}-${i}`;
    const t0 = performance.now();
    try {
      const url = await render(title);
      const img = await fetch(url);
      if (!img.ok) throw new Error(`download ${img.status}`);
      await img.arrayBuffer();
      times.push(performance.now() - t0);
    } catch (e) { failures++; console.error(name, e.message); }
  }
  console.log(name, `ok ${times.length}/10`, `median ${Math.round(median(times))} ms`, `max ${Math.round(Math.max(...times))} ms`, `failures ${failures}`);
}

The titles in this script are generated, so they are safe to put into HTML without escaping. Escape real user input as in the first HCTI example. Ten renders use ten credits on each service; the free tiers cover one run. Count any Bannerbear 408 response as a failure: it means the render took longer than the sync host allows.

When HCTI is the better choice

  • Your cards already exist as HTML, or your team prefers to design in CSS.
  • You need URL screenshots and HTML renders from one key.
  • You want a predictable synchronous call in a request path, such as an OG image endpoint.
  • Price per image matters: HCTI was cheaper at all three volumes.

When Bannerbear is the better choice

  • Marketing or design owns the template and must change it without engineers.
  • You need the same template as an image, an animation, a video or a PDF.
  • You run multi-step jobs (render, resize, add subtitles) and want them in one workflow call.
  • You connect through no-code tools, a hosted form, or an AI assistant over MCP, which Bannerbear supports.

If you are weighing Bannerbear against another template tool, see Bannerbear vs Placid. If you are weighing HCTI against one, see Placid vs HTML/CSS to Image.

Where RenderScreenshot fits

RenderScreenshot sits on the HCTI side of this split: you send HTML (or a URL) and it renders in Chromium. The og_card preset sets a 1200x630 viewport, and every result is cached with a public link in the X-Cache-URL header. HTML input is described in content input and the presets in the presets reference.

// RenderScreenshot: render your own HTML template as a 1200x630 PNG.
import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) =>
  ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);

const client = new Client(process.env.RS_API_KEY);
const title = 'Same template, two renderers';
const html = `<div style="width:1200px;height:630px;display:flex;align-items:center;padding:72px;
  box-sizing:border-box;background:#312e81;color:#fff;font:700 64px system-ui">${escapeHtml(title)}</div>`;

const png = await client.take(TakeOptions.html(html).preset('og_card'));
await writeFile('card-rs.png', png);
console.log('saved card-rs.png');

We tested this snippet against a local mock of our API, not the live service. We have no visual template editor, no video and no workflows. If those matter, Bannerbear covers them and we do not. To add a third column to the benchmark above, get a free API key.

Last checked 2026-10-15: HCTI and Bannerbear API docs and pricing pages. Request code run with Node 22.18.0 against a local mock that checks each documented auth scheme and required field.

Frequently asked questions

Can Bannerbear render my own HTML?
Not through its image API. Bannerbear renders templates designed in its editor, and your API call changes the layers by name: text, image URLs, colors. If you want to send HTML and CSS, HTML/CSS to Image (or a screenshot API) is the right tool.
Is HTML/CSS to Image only for developers?
Mostly. Its core API takes HTML and CSS, and its saved templates are HTML with Handlebars variables. It now also has a visual template editor, but its strength is that anything a browser can render, it can render.
Which is cheaper for 10,000 images a month?
On list prices checked on 2026-10-15, HTML/CSS to Image's Basic plan with 10,000 images is $69 a month and Bannerbear's plan with 10,000 API credits is $149 a month. Bannerbear multiplies credits by the number of formats and by the scale you request.
Do both return a hosted image URL?
Yes. HTML/CSS to Image returns a permanent URL on hcti.io that you can change to .png, .jpg, .webp or .pdf. Bannerbear returns file URLs per format once the image status is completed.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.