Documentation
Render anything in three calls.
Authenticate with a key, then send a URL or HTML. Every endpoint returns the file directly — no polling, no webhooks, no SDK to install.
Quickstart
Create a free key (email-verified, 500 renders a month, no card), then make your first call. The response body is the image.
# a full-page screenshot, saved as PNG curl -X POST https://renderhelm.keelhelm.com/v1/screenshot \ -H "Authorization: Bearer sk_your_key" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com","full_page":true}' \ --output shot.png
// app/api/shot/route.ts — Next.js route handler export async function GET() { const r = await fetch("https://renderhelm.keelhelm.com/v1/screenshot", { method: "POST", headers: { Authorization: `Bearer ${process.env.RENDERHELM_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://example.com" }), }); return new Response(r.body, { headers: { "Content-Type": "image/png" } }); }
// src/pages/shot.png.ts — Astro endpoint export const GET = async () => fetch("https://renderhelm.keelhelm.com/v1/screenshot", { method: "POST", headers: { Authorization: `Bearer ${import.meta.env.RENDERHELM_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url: "https://example.com" }), });
Authentication
Pass your secret key as a bearer token: Authorization: Bearer sk_…. Keys are created at signup and scoped to your account. For Open Graph images you embed in HTML, use your public key plus a signature (see below) so the secret never appears in a URL.
Screenshot
POST /v1/screenshot
Capture any URL or your own HTML as a PNG or JPEG. Viewport by default; full-page on request.
| Field | Type | Notes |
|---|---|---|
url / html | string | One is required (not both). |
full_page | boolean | Capture the whole scroll height. Counts as 2 renders. |
format | png · jpeg | Defaults to png. |
viewport_width / viewport_height | number | Defaults 1280 × 800. |
response | binary · url | Image bytes, or a JSON link to the cached result. |
POST /v1/pdf
Turn a live page or your own markup into a paginated PDF. Counts as 2 renders.
| Field | Type | Notes |
|---|---|---|
url / html | string | One is required. |
format | A4 · Letter | Defaults to A4. |
landscape | boolean | Defaults to false. |
margin_mm | number | Uniform page margin in millimetres. |
curl -X POST https://renderhelm.keelhelm.com/v1/pdf \ -H "Authorization: Bearer sk_your_key" -H "Content-Type: application/json" \ -d '{"url":"https://example.com","format":"A4"}' --output page.pdf
Render
POST /v1/render
Send your own HTML and get back a PNG, JPEG, or PDF. The HTML can be a finished document or a template with variables and loops — RenderHelm expands it server-side, then renders the result.
| Field | Type | Notes |
|---|---|---|
html | string | Required, up to 2 MB. May contain the template tags below. |
vars | object | Values the template references. |
format | png · jpeg · pdf | Defaults to png. PDF counts as 2 renders. |
css | string[] | Up to 5 https stylesheet URLs, linked into the page. |
css_id | string | A stored private stylesheet — see Private CSS. |
full_page · viewport_width · viewport_height · dark_mode · response | — | Same as Screenshot. |
Template syntax
| Tag | Meaning |
|---|---|
{{ path.to.value }} | Insert a value — HTML-escaped by default, so user data can't inject markup. Missing values render empty. |
{{ value | raw }} | Insert without escaping. Only use with content you trust — | raw can inject arbitrary markup. |
{% if path %}…{% else %}…{% endif %} | Show a block when the value is truthy; {% if not path %} negates. |
{% for item in list %}…{% endfor %} | Repeat for each array item. loop.index, loop.first, and loop.last are available inside. |
Per render: up to 50k tags, 20 levels of nesting, 10k total loop iterations, and 2 MB of output. A malformed template returns template_syntax_error; exceeding a limit returns template_limits_exceeded.
curl -X POST https://renderhelm.keelhelm.com/v1/render \ -H "Authorization: Bearer sk_your_key" -H "Content-Type: application/json" \ -d '{"html":"<h1>Hi {{ name }}</h1>","vars":{"name":"there"}}' --output card.png
Private CSS
POST /v1/css · Pro & Scale
Store your brand stylesheet once, then reference it from any render with css_id. Your styles are injected server-side, so they never appear in a URL and load instantly. Available on the Pro and Scale plans.
| Route | Does |
|---|---|
POST /v1/css | Create or overwrite: {"name":"brand","content":"…css…"}. Name is [a-z0-9_-], content up to 256 KB. |
GET /v1/css | List your stored stylesheets. |
DELETE /v1/css/:name | Delete one. |
Up to 20 stylesheets per account. On a Free or Starter key these routes return tier_required; referencing a css_id that doesn't exist at render time returns css_not_found.
Open Graph image
GET /v1/og/:template.png
Generate the preview image that appears when a link is shared, from a reusable template. Because it's a plain GET, you embed it with a single tag — pass your public key and a signature so the URL is safe to ship in your HTML.
<!-- in your page <head> --> <meta property="og:image" content="https://renderhelm.keelhelm.com/v1/og/blog-header.png?title=Hello&pk=pk_you&sig=…">
Browse the template gallery for every template, its parameters, and a live example. Identical parameters return instantly from cache.
Signed GET embeds
Some renders are plain GET URLs you can drop straight into an <img> or <meta> tag — an Open Graph image, or a screenshot embed. Because the URL is visible, you never put your secret key in it. Instead you sign it with your public key: append pk=<your public key> and sig=<signature>, where the signature is an HMAC-SHA256 of the request path plus the query string — sorted, with sig removed — using your signing secret (shown in the portal). The public key and signing secret are safe to ship; the secret key stays server-side.
<!-- an Open Graph image, embedded in your <head> --> <meta property="og:image" content="https://renderhelm.keelhelm.com/v1/og/blog-header.png?title=Hello&pk=pk_you&sig=…"> # a signed screenshot embed (same signing scheme) GET /v1/screenshot?url=https://example.com&pk=pk_you&sig=…
Portal
Your account portal manages everything without touching the API: sign in with a one-time email link — no password — then create and label up to five keys, roll a key to reveal a fresh secret if you've lost one, watch your monthly usage, and run any endpoint from the built-in playground. The playground meters against your plan exactly like a real call, and your secret key never leaves the server.
Cache
On a paid plan, every render is stored in your account's private cache. Ask for the same render again and it's served from cache in milliseconds for free — a cache hit costs zero units. Cached items live 6 months and the clock resets on every hit, so what you reuse stays warm. When your cache is full the least-recently-used items are evicted to make room; if only pinned items remain, the new render simply isn't cached. The free plan has no cache — every free render is fresh and counted.
Two flags on any render request control caching, and a header tells you the hash to manage:
| Field | Effect |
|---|---|
fresh | Skip the cache lookup and re-render — but still store the result. |
no_cache | Don't read or write the cache at all: a fresh, billed render that isn't stored. |
response: "url" | Return a JSON link to the cached object instead of the bytes (paid plans only). |
X-Render-Hash | Response header — the hash you pin or delete below. |
GET /v1/cache
Manage your cache from the portal or the API (Bearer sk_).
| Route | Does |
|---|---|
GET /v1/cache | List cached renders + usage (bytes used / limit, item count). |
DELETE /v1/cache/:hash | Delete one cached render. |
POST /v1/cache/:hash/pin · /unpin | Pin (never evict or expire) or unpin an entry. |
Limits & units
A screenshot or Open Graph image is one render; a full-page capture, a PDF, or a Compose render to PDF is two; a cache hit is zero. Plans set a monthly render budget plus a per-minute rate — see pricing. Go over your plan and, unless you've switched on metered overage ($1.50 per 1,000 renders, off by default), new renders pause until your next cycle. On paid plans, identical requests are served from your cache and never counted (a cached item is re-billed only after 6 months of no use, or if you evict it); the free plan has no cache, so every free render counts.
Errors
Every error returns JSON: {"error":{"code","message","doc_url"}} with the right HTTP status.
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | Missing or invalid key. |
bad_signature | 401 | Open Graph signature didn't match the parameters. |
blocked_target | 400 | The target URL isn't allowed (private/reserved address). |
rate_limited | 429 | Too many requests this minute — slow down. |
quota_exceeded | 429 | Monthly render budget reached. |
payload_too_large | 413 | Inline HTML exceeds 2 MB. |
template_syntax_error | 400 | A /v1/render template couldn't be parsed (with an offset). |
template_limits_exceeded | 400 | A template exceeded a size, nesting, loop, or output limit. |
tier_required | 403 | Private CSS needs a Pro or Scale plan. |
css_not_found | 404 | The referenced css_id doesn't exist for your account. |
css_limit_reached | 409 | You already have 20 stored stylesheets. |
demo_requires_key | 400 | The demo only renders its ready-made examples — get a free key for your own. |
render_failed | 500 | The page couldn't be rendered — retry. |