HTTP API
Two POST endpoints, a bearer key, and a binary response. Everything the render API does.
The API base URL is https://api.snhtml.com. Every render is a single POST with a JSON body; the response body is the rendered file itself.
Authentication
Send your key as a bearer token. Keys are created in the dashboard under API Keys and are shown only once.
authorization: Bearer snhtml_...
content-type: application/json
A missing, malformed, disabled, or unknown key returns 401 with {"error":"Invalid API key"}. Keys carry your account’s quota, so treat them as server-side secrets — never ship one in a browser bundle or mobile app.
POST /v1/render
Renders HTML you send, or a URL you point at.
Provide html or url — at least one is required. If both are present, html is used.
curl -X POST https://api.snhtml.com/v1/render \
-H "authorization: Bearer snhtml_..." \
-H "content-type: application/json" \
-d '{
"html": "<main><h1>Invoice #1842</h1></main>",
"format": "pdf",
"pdfFormat": "A4",
"printBackground": true,
"fileName": "invoice-1842"
}' \
--output invoice-1842.pdf
Rendering a live page instead:
{
"url": "https://example.com/reports/2026-q1",
"format": "png",
"width": 1440,
"height": 900,
"fullPage": true,
"waitUntil": "networkidle"
}
The page is fetched by the renderer, not by your server, so it must be reachable from the public internet. Anything behind a login, a VPN, or an IP allowlist won’t render — build the HTML yourself and send it as html instead.
POST /v1/render/template
Renders a template you saved in the dashboard, with {{ variable }} placeholders replaced.
templateId is required. html and url are not accepted on this endpoint — the template is the source.
curl -X POST https://api.snhtml.com/v1/render/template \
-H "authorization: Bearer snhtml_..." \
-H "content-type: application/json" \
-d '{
"templateId": "k57d2...",
"variables": {
"customerName": "Ada Lovelace",
"invoiceNumber": 1842,
"total": "$249.00"
},
"format": "pdf"
}' \
--output invoice-1842.pdf
See Templates for how templates and variables work.
Every other field is shared with /v1/render — see Render options for the full list.
Response
On success you get the bytes, not JSON:
| Header | Value |
|---|---|
content-type |
image/png, image/webp, image/jpeg, or application/pdf |
content-disposition |
attachment; filename="<fileName>.<ext>" |
cache-control |
no-store |
The filename comes from fileName, slugified and capped at 80 characters, with the format’s extension appended. Without fileName, /v1/render falls back to render and /v1/render/template falls back to the template’s name.
On failure you get JSON with an error string and a status code — see Errors.
const response = await fetch("https://api.snhtml.com/v1/render", {
method: "POST",
headers: {
authorization: `Bearer ${process.env.SNAPHTML_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({ html, format: "pdf" }),
});
if (!response.ok) {
const { error } = (await response.json()) as { error: string };
throw new Error(error);
}
const pdf = Buffer.from(await response.arrayBuffer());
Limits per request
| Limit | Value |
|---|---|
| HTML payload | 5 MB |
waitForTimeout |
10,000 ms |
deviceScaleFactor |
4 |
scale (PDF) |
0.1 – 2 |
fileName |
160 characters |
Unknown fields are rejected rather than ignored, so a typo like pageFormat fails with 400 instead of silently rendering the wrong thing.