Private beta— the API is live and free. Grab a key below and start rendering.
RenderHelm.

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.

FieldTypeNotes
url / htmlstringOne is required (not both).
full_pagebooleanCapture the whole scroll height. Counts as 2 render tokens.
formatpng · jpegDefaults to png.
viewport_width / viewport_heightnumberDefaults 1280 × 800.
responsebinary · urlImage bytes, or a JSON link to the cached result.

PDF

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).

FieldTypeNotes
url / htmlstringOne is required.
pdf_formatA4 · Letter · Legal · A3 · TabloidNamed page size. Defaults to A4. (format is accepted as an alias.)
width_mm · height_mmnumberCustom page size in millimetres — set both to override pdf_format (10–2000 mm).
landscapebooleanRotate to landscape. Defaults to false.
margin_mmnumberUniform page margin in millimetres (0–50). Defaults to 10.
margin_top_mm · margin_right_mm · margin_bottom_mm · margin_left_mmnumberPer-side margins (0–100) — each overrides the uniform margin_mm.
header_html · footer_htmlstringRunning 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_numbersbooleanShortcut: prints a centered N / total footer.
prefer_css_page_sizebooleanHonor 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.

FieldTypeNotes
htmlstringRequired, up to 2 MB. May contain the template tags below.
varsobjectValues the template references.
formatpng · jpeg · pdfDefaults to png. PDF is priced by length — see PDF.
cssstring[]Up to 5 https stylesheet URLs, linked into the page.
css_idstringA stored private stylesheet — see Private CSS.
full_page · viewport_width · viewport_height · responseSame 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

TagMeaning
{{ 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.

RouteDoes
POST /v1/cssCreate or overwrite: {"name":"brand","content":"…css…"}. Name is [a-z0-9_-], content up to 256 KB.
GET /v1/cssList your stored stylesheets.
DELETE /v1/css/:nameDelete 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:

FieldEffect
freshSkip the cache lookup and re-render — but still store the result.
no_cacheDon'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-HashResponse header — the hash you pin or delete below.
GET /v1/cache

Manage your cache from the portal or the API (Bearer sk_).

RouteDoes
GET /v1/cacheList cached renders + usage (bytes used / limit, item count).
DELETE /v1/cache/:hashDelete one cached render.
POST /v1/cache/:hash/pin · /unpinPin (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.

CodeStatusMeaning
unauthorized401Missing or invalid key.
bad_signature401Open Graph signature didn't match the parameters.
blocked_target400The target URL isn't allowed (private/reserved address).
rate_limited429Too many requests this minute — slow down.
quota_exceeded429Monthly render budget reached.
payload_too_large413Inline HTML exceeds 2 MB.
template_syntax_error400A /v1/render template couldn't be parsed (with an offset).
template_limits_exceeded400A template exceeded a size, nesting, loop, or output limit.
tier_required403Private CSS needs a Pro or Scale plan.
css_not_found404The referenced css_id doesn't exist for your account.
css_limit_reached409You already have 20 stored stylesheets.
demo_requires_key400The demo only renders its ready-made examples — get a free key for your own.
terms_required400Creating a key requires agreeing to the Terms of Service and Privacy Policy (send terms_accepted: true).
render_failed500The page couldn't be rendered — retry.