Disclosure: I build RenderScreenshot, which can also generate social images from HTML templates. That makes both products indirect competitors. I have not rendered with either service for this page: the comparison below is built from their API references and pricing pages (read on 2026-10-15), and the test script is untested against live accounts. I note where each one is the better choice.
Short answer: both are template engines with an API. You design a template in their editor, then send text and images to fill named layers. Choose Placid if you want simple credit maths (one image, one credit), a URL API you can drop into og:image tags, a synchronous create_now option, and a template layer that can embed a live website screenshot. Choose Bannerbear if you also need animations, multi-step workflows, AI image tools or an MCP server for agents, and you are comfortable with credits that multiply by format and scale.
Neither is a screenshot API. If what you need is a picture of a web page, not a designed card, see the screenshot tool comparisons instead.
How each API is shaped
| Bannerbear | Placid | |
|---|---|---|
| Base API | https://api.bannerbear.com/v5 |
https://api.placid.app/api/rest |
| Auth | Authorization: Bearer with a V5 key; keys can be scoped (for example images:write) and restricted to browser origins |
Authorization: Bearer with a project-specific token |
| Create an image | POST /v5/images with template and modifications.objects[] |
POST /images with template_uuid and a layers object |
| Default flow | Async: 202 Accepted, then poll GET /v5/images/:uid or receive a webhook |
Async: status: "queued", then poll polling_url or receive webhook_success |
| Synchronous option | Same request to https://sync.api.bannerbear.com; 10-second limit, 408 on timeout |
"create_now": true in the body; can fail when your worker is busy |
URL-based images (for og:image) |
Instant URLs, HMAC signed, bound to one template; listed on Scale and above | URL API: https://api.placid.app/u/{template_uuid}?layer[text]=..., optional HMAC signature in &s= |
| Output formats | JPG, PNG, WebP, AVIF, PDF, set per request in formats[] |
auto (JPG or PNG), JPG, PNG, WebP via modifications.image_format |
| Resolution | scale 1 to 4 |
Width and height modifications, DPI for print |
| Storage | Hosted file URLs; custom S3 on Scale and above | Hosted URL; transfer to your S3 bucket in the request |
| Website capture | v2 POST /v2/screenshots (url, width, height, mobile, language) |
Browserframe layer: image can be a website URL, image_viewport defaults to 1280x1024 |
| Beyond images | Animations (MP4), workflows, AI tools, MCP server | PDFs, videos, Editor SDK, MCP |
| Rate limit (documented) | 60 POST requests per 10 seconds | See Placid's rate-limit page |
Sources: Bannerbear API reference, Bannerbear v2 reference, Placid image API, Placid layers, Placid URL API.
Two details stand out when you write the integration:
- Bannerbear v5 keys do not work on v2 endpoints, and the reverse. If you depend on the v2 screenshot endpoint, you keep a v2 key for it.
- Placid's
create_nowis not a guarantee. Its docs say it may fail when too many requests run at once. Keep the polling path as a fallback, as the script below does.
Credits: the rule that changes your bill
Bannerbear: one image render is one credit, multiplied by the number of formats and by the scale. Ask for JPG and PNG at scale 2, and one render costs four credits. AI features (background removal, subject detection) add one credit per layer that uses them.
Placid: one image is one credit for canvases up to 4,000 px. A PDF page is two credits, and video costs one credit per second. Previews in test mode are free and watermarked.
So the first habit with Bannerbear is to request exactly one format at the scale you need. If you need a PNG for one platform and a JPG for another, compare the cost of two formats against converting one file yourself.
Price at three volumes
One output format, scale 1, list prices as shown on each pricing page on 2026-10-15:
| Images per month | Bannerbear plan | Placid plan |
|---|---|---|
| 1,000 | Automate, $49/mo (1,000 API credits) | Pro (2,500 credits/mo) |
| 10,000 | Scale, $149/mo (10,000 API credits) | Business (25,000 credits/mo) |
| 100,000 | Enterprise, from $299/mo for 50,000 credits; 100,000 is a higher tier on the same plan | VIP (100,000 credits/mo) |
Placid's page renders its dollar prices with JavaScript, and our text capture did not include them, so check Placid's pricing page for the current numbers. Bannerbear prices are from its pricing page; it offers a trial with 30 API credits.
A script to test both on your template
The fair way to compare is to build the same template in both editors (same size, same fonts, layers named title and image), then render the same inputs. Use three inputs: a short title, a long title that must wrap or shrink, and a title with emoji and Japanese text.
This Node 22 script renders one input on each service, prefers the synchronous path, falls back to polling, downloads the result, and prints timing and size. It has not been run against live accounts for this page.
// Render the same data with Bannerbear and Placid, then download and time each result. // Env: BANNERBEAR_API_KEY, BANNERBEAR_TEMPLATE, PLACID_TOKEN, PLACID_TEMPLATE import { writeFile } from 'node:fs/promises'; const input = { title: process.argv[2] ?? 'Quarterly report: what changed in October', image: process.argv[3] ?? 'https://example.com/cover.jpg', }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function json(res) { const body = await res.json().catch(() => ({})); if (!res.ok) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body).slice(0, 300)}`); return body; } async function poll(url, headers, isDone, getUrl) { for (let i = 0; i < 30; i++) { const body = await json(await fetch(url, { headers, signal: AbortSignal.timeout(15_000) })); if (isDone(body)) return getUrl(body); if (body.status === 'failed' || body.status === 'error') throw new Error(`render failed: ${JSON.stringify(body).slice(0, 300)}`); await sleep(1000); } throw new Error('render did not finish in 30 s'); } async function bannerbear() { const headers = { Authorization: `Bearer ${process.env.BANNERBEAR_API_KEY}`, 'Content-Type': 'application/json' }; const body = JSON.stringify({ template: process.env.BANNERBEAR_TEMPLATE, formats: ['png'], // each extra format or scale step multiplies the credit cost modifications: { objects: [ { name: 'title', text: input.title }, { name: 'image', 'background-image': input.image }, ] }, }); let res = await fetch('https://sync.api.bannerbear.com/v5/images', { method: 'POST', headers, body, signal: AbortSignal.timeout(15_000) }); // A 408 means the sync host gave up waiting; retry on the async host (check usage: the first attempt may still count). if (res.status === 408) res = await fetch('https://api.bannerbear.com/v5/images', { method: 'POST', headers, body }); const image = await json(res); if (image.status === 'completed') return image.files.png; return poll(image.self, headers, (b) => b.status === 'completed', (b) => b.files.png); } async function placid() { const headers = { Authorization: `Bearer ${process.env.PLACID_TOKEN}`, 'Content-Type': 'application/json' }; const image = await json(await fetch('https://api.placid.app/api/rest/images', { method: 'POST', headers, signal: AbortSignal.timeout(30_000), body: JSON.stringify({ template_uuid: process.env.PLACID_TEMPLATE, create_now: true, // may still come back queued when busy layers: { title: { text: input.title }, image: { image: input.image } }, modifications: { image_format: 'png' }, }), })); if (image.status === 'finished') return image.image_url; return poll(image.polling_url, headers, (b) => b.status === 'finished', (b) => b.image_url); } for (const [name, render] of Object.entries({ bannerbear, placid })) { const t0 = performance.now(); try { const url = await render(); const t1 = performance.now(); const file = Buffer.from(await (await fetch(url)).arrayBuffer()); await writeFile(`${name}.png`, file); console.log(`${name}: render ${(t1 - t0).toFixed(0)} ms, download ${(performance.now() - t1).toFixed(0)} ms, ${file.length} bytes`); } catch (err) { console.error(`${name}: ${err.message}`); process.exitCode = 1; } }
Run it with each of your three inputs, at least five times each, at different times of day. Report:
- Render time, from request to a finished file URL. Async queues make the spread more useful than the median.
- Download time and bytes of the result, since both hand you a hosted file.
- Text fitting: does the long title wrap, shrink or overflow? Does the CJK and emoji title render, or show boxes? This depends on the fonts you uploaded, so use the same font files in both.
- Failures: count 408s from Bannerbear's sync host and
queuedresponses from Placid'screate_now.
We will add measured results to this page when we have run the script on matched templates.
Where each one wins
Placid is the better fit when:
- You want
og:imageURLs that render on first request, signed with HMAC. - You want to know the cost of a render before you send it.
- You need PDFs and videos from the same templates, or an embeddable template editor for your own users (Editor SDK).
- You want a website screenshot placed inside a designed frame.
Bannerbear is the better fit when:
- You need animations, or multi-step workflows that chain renders and tools.
- You want AI features such as background removal inside the render.
- Your team works with agents through MCP, and a hosted MCP server matters.
- You need fine control over API key scopes and allowed browser origins.
Neither is the right tool when your source is a web page or HTML you already have. A template editor duplicates your design work; rendering your own HTML keeps one source of truth.
The HTML route: render your own template
If your cards are built from your site's own design system, write the template as HTML and render it with a headless browser. With RenderScreenshot, the og_card preset sets a 1200x630 viewport, and the HTML input takes the markup directly:
import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); const title = escapeHtml(process.argv[2] ?? 'Quarterly report: what changed in October'); const html = `<!doctype html><html><body style="margin:0;width:1200px;height:630px;display:flex;align-items:center; padding:0 80px;box-sizing:border-box;background:#0f172a;color:#fff;font:700 64px/1.1 system-ui,sans-serif">${title}</body></html>`; const client = new Client(process.env.RS_API_KEY); await writeFile('card.png', await client.take(TakeOptions.html(html).preset('og_card')));
The escape step matters: the title is user data, and without it a title like <img src=x onerror=...> becomes markup in your template. For a signed URL you can put straight into a meta tag, use client.generateUrl() with an expiry, as described in signed URLs. Our OG image API page covers the full workflow, and you can get a free API key to try it.
Last verified: 2026-10-15 (API references and pricing pages). Measured results pending.