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
titleandphoto.
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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.