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 render tokens 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
# pip install requests — saves a full-page screenshot to shot.png import os, requests r = requests.post( "https://renderhelm.keelhelm.com/v1/screenshot", headers={"Authorization": f"Bearer {os.environ['RENDERHELM_KEY']}"}, json={"url": "https://example.com", "full_page": True}, ) with open("shot.png", "wb") as f: f.write(r.content)
// Node 18+ — built-in fetch, zero dependencies import { writeFile } from "node:fs/promises"; 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", full_page: true }), }); await writeFile("shot.png", Buffer.from(await r.arrayBuffer()));
// Java 11+ — java.net.http, no dependencies import java.net.URI; import java.net.http.*; import java.nio.file.Path; var client = HttpClient.newHttpClient(); var req = HttpRequest.newBuilder(URI.create("https://renderhelm.keelhelm.com/v1/screenshot")) .header("Authorization", "Bearer " + System.getenv("RENDERHELM_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"url\":\"https://example.com\",\"full_page\":true}")) .build(); client.send(req, HttpResponse.BodyHandlers.ofFile(Path.of("shot.png")));
# net/http and json ship with Ruby — no gems require "net/http"; require "json" uri = URI("https://renderhelm.keelhelm.com/v1/screenshot") req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{ENV['RENDERHELM_KEY']}", "Content-Type" => "application/json") req.body = { url: "https://example.com", full_page: true }.to_json res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) } File.binwrite("shot.png", res.body)
// .NET — System.Net.Http, no packages using var http = new HttpClient(); var req = new HttpRequestMessage(HttpMethod.Post, "https://renderhelm.keelhelm.com/v1/screenshot"); req.Headers.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable(\"RENDERHELM_KEY\")}"); req.Content = new StringContent("{\"url\":\"https://example.com\",\"full_page\":true}", System.Text.Encoding.UTF8, "application/json"); var res = await http.SendAsync(req); await File.WriteAllBytesAsync("shot.png", await res.Content.ReadAsByteArrayAsync());
// 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 render tokens. |
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. A PDF is priced by length — 2 render tokens for up to 10 pages, then +1 per additional 5 pages (a cache hit is free).
| Field | Type | Notes |
|---|---|---|
url / html | string | One is required. |
pdf_format | A4 · Letter · Legal · A3 · Tabloid | Named page size. Defaults to A4. (format is accepted as an alias.) |
width_mm · height_mm | number | Custom page size in millimetres — set both to override pdf_format (10–2000 mm). |
landscape | boolean | Rotate to landscape. Defaults to false. |
margin_mm | number | Uniform page margin in millimetres (0–50). Defaults to 10. |
margin_top_mm · margin_right_mm · margin_bottom_mm · margin_left_mm | number | Per-side margins (0–100) — each overrides the uniform margin_mm. |
header_html · footer_html | string | Running header/footer markup printed on every page. Use the classes pageNumber, totalPages, title, date, url to inject those values (e.g. <span class="pageNumber"></span>). Give the header/footer inline font-size, and leave room with a larger top/bottom margin. |
page_numbers | boolean | Shortcut: prints a centered N / total footer. |
prefer_css_page_size | boolean | Honor the page's own CSS @page size and margins. Combine with @page:first { … } to style the first page differently from the rest. |
A PDF costs 2 render tokens for up to 10 pages, then +1 per additional 5 pages; a cache hit is free. Header/footer markup renders in the same isolated browser as your page — no external fetch, no secrets in the URL.
# a Letter-size invoice, landscape, with page numbers 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","pdf_format":"Letter","landscape":true,"page_numbers":true}' --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 is priced by length — see PDF. |
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 · response | — | Same as Screenshot. |
When format is pdf, all the PDF options apply too — page size, per-side margins, running headers/footers, page numbers, and prefer_css_page_size.
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 render tokens. 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 & render tokens
Every render spends render tokens. A screenshot or Open Graph image is one; a full-page capture is two; a PDF (captured or composed) is priced by length — two tokens for up to 10 pages, then one more per additional 5 pages; a cache hit is zero. Plans set a monthly render-token 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 render tokens, 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. |
terms_required | 400 | Creating a key requires agreeing to the Terms of Service and Privacy Policy (send terms_accepted: true). |
render_failed | 500 | The page couldn't be rendered — retry. |