Reference
API quickstart
Send HTML, a URL, or a saved template — get a PDF or image back. Render synchronously, or async for big jobs. Authenticate with a bearer token.
Prefer to click around? The interactive API reference renders every endpoint from the OpenAPI spec with a built-in “try it” client.
Uses the public demo key (rate-limited). Create your own key in the dashboard for production.
Authentication
Pass your key in the Authorization header. The on-site playground uses a public demo key capped at 5 requests a minute and 40a day, and only from this site — enough to try the API, not to build on. A free key raises that to 120 a minute and 500 documents a month, and unlocks templates and async jobs.
Authorization: Bearer sk_live_your_keyGenerate a PDF
POST /api/v1/pdf
Content-Type: application/json
{
"html": "<h1>Hello, PDF</h1>",
"options": { "format": "A4", "printBackground": true }
}Or render a live URL:
curl https://rendral.com/api/v1/pdf \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com" }' \
--output page.pdfHeaders, footers & page numbers
Set pageNumbers: truefor a centered “N / total” footer, or supply your own header/footer HTML using Puppeteer's injected classes — pageNumber, totalPages, date, title.
{
"html": "<h1>Report</h1>",
"options": {
"pageNumbers": true,
"headerTemplate": "<div style='font-size:9px;width:100%;text-align:right;padding:0 12px'>Acme Inc.</div>",
"displayHeaderFooter": true,
"margin": { "top": "0.8in", "bottom": "0.8in" }
}
}Waiting for content
For pages that render asynchronously (charts, fonts, client-side data), control when capture happens.
{
"url": "https://dashboard.example.com/report",
"options": {
"waitUntil": "networkidle0",
"waitForSelector": "#chart.ready",
"delay": 500
}
}PDF options
| Field | Type | Default |
|---|---|---|
| format | string | A4 |
| width / height | string | — (overrides format) |
| landscape | boolean | false |
| printBackground | boolean | true |
| scale | number | 1 |
| margin | object | 0.4in all sides |
| pageNumbers | boolean | false |
| headerTemplate / footerTemplate | string (HTML) | — |
| pageRanges | string | e.g. "1-3, 5" |
| preferCSSPageSize | boolean | false |
| waitUntil | string | networkidle0 |
| waitForSelector | string | — |
| delay | number (ms) | 0 |
| emulateMediaType | "screen" | "print" | |
| cache | boolean | true |
Getting a blank PDF from a URL?
Chrome renders PDFs with the page’s printstylesheet, the same as “Print to PDF” in your browser. Plenty of sites hide most of the page in @media print, so the render succeeds and returns a valid, empty document — a 200 with nothing on it. Ask for the screen stylesheet instead:
{
"url": "https://example.com",
"options": { "emulateMediaType": "screen" }
}That is the right default for your own invoice or report HTML, which is why it stays the default. If screen media doesn’t help, the page is probably built after load — add waitForSelector for an element that only exists once the content is there. A few sites also refuse headless browsers outright, and no option fixes that.
Screenshots
Same inputs, an image back — PNG, JPEG or WebP. Capture the full page, the viewport, or a single element.
POST /api/v1/screenshot
{
"url": "https://example.com",
"options": {
"type": "png",
"fullPage": true,
"viewport": { "width": 1280, "height": 800 }
}
}Capture one element and keep transparency:
{
"html": "<div id='card'>…</div>",
"options": { "type": "png", "selector": "#card", "omitBackground": true }
}| Field | Type | Default |
|---|---|---|
| type | "png" | "jpeg" | "webp" | png |
| fullPage | boolean | false |
| selector | string | — (whole page) |
| quality | number (jpeg/webp) | 80 |
| omitBackground | boolean | false |
| clip | { x, y, width, height } | — |
| viewport | { width, height } | 800 × 600 |
| deviceScaleFactor | number | 1 |
Open Graph images
POST /api/v1/og renders a 1200×630 social image. Pass a few fields for the built-in template, or your own html for full control.
{
"title": "Ship PDFs without a browser",
"description": "One API call. Real Chrome rendering.",
"eyebrow": "Rendral",
"theme": "dark"
}Merge & watermark
Combine PDFs or stamp a watermark — pure and fast, no browser involved. Send PDFs as base64.
POST /api/v1/merge
{ "files": ["<base64 pdf>", "<base64 pdf>"] }
POST /api/v1/watermark
{ "file": "<base64 pdf>", "text": "CONFIDENTIAL",
"options": { "opacity": 0.15, "rotate": 45 } }PDF page tools
Reorder, extract, rotate, split, and inspect existing PDFs — pure and fast, no browser. Page specs are 1-based; ranges may run either direction and pages may repeat (3,1,2,5-7).
# Keep pages 3,1,2 then 5–7, rotate 90°, set the title → PDF out
POST /api/v1/pdf/pages
{ "file": "<base64 pdf>", "pages": "3,1,2,5-7", "rotate": 90,
"metadata": { "title": "Q3 report" } }
# One PDF per page → { "count": 12, "files": ["<base64>", …] }
POST /api/v1/pdf/split
{ "file": "<base64 pdf>" }
# Inspect → { "pageCount": 12, "pages": [{ "width", "height", "rotation" }], "metadata": {…} }
POST /api/v1/pdf/info
{ "file": "<base64 pdf>" }Every rendered PDF is tagged (accessible / PDF-UA) by default — pass "options": { "tagged": false } to /api/v1/pdf to opt out.
Fill a PDF form
Fills an AcroForm and returns the document. Ask /api/v1/pdf/infofor the field names first — whoever made the PDF rarely documents them, and a wrong name comes back with the real ones.
POST /api/v1/pdf/info
{ "file": "<base64 PDF>" }
# → { "formFields": [
# { "name": "applicant.name", "type": "text" },
# { "name": "country", "type": "dropdown", "options": ["UK","US","DE"] }
# ], ... }
POST /api/v1/pdf/form
{
"file": "<base64 PDF>",
"fields": { "applicant.name": "Ada Lovelace", "country": "UK", "terms.accepted": true },
"flatten": true
}Text fields take a string, checkboxes a boolean, radio groups and dropdowns one of the values the field offers, list fields an array. flattenbakes the values into the page and removes the widgets, so the result can’t be edited back — what you want for a signed application, not for a draft.
Reusable templates
Store an HTML template once, then render it by name with JSON merge data — no need to ship full HTML on every call. Fields are {{name}} (HTML-escaped) or {{{name}}} (raw); dotted paths like {{customer.name}} read nested objects. Requires an account API key.
# Save a template
curl -X PUT https://rendral.com/api/v1/templates/invoice \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "html": "<h1>{{company}}</h1><p>Total: {{amount}}</p>" }'
# Render it with data
curl https://rendral.com/api/v1/pdf \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "template": "invoice", "data": { "company": "Acme", "amount": "$1,240" } }' \
--output invoice.pdfManage templates with GET /api/v1/templates (list) and GET / PUT / DELETE /api/v1/templates/{name}.
Async rendering & webhooks
For big or batch documents that might exceed the request window, add "async": true and get a job id back immediately. Poll it, or pass a webhook URL to be notified on completion. Requires an account API key.
POST /api/v1/pdf
{ "url": "https://example.com/big-report",
"async": true,
"webhook": "https://you.example.com/hooks/rendral" }
→ 202 { "id": "job_ab12…", "status": "queued",
"poll_url": "https://rendral.com/api/v1/jobs/job_ab12…" }
GET /api/v1/jobs/job_ab12…
→ { "job": { "status": "done",
"output_url": ".../api/v1/jobs/job_ab12…/output" } }The webhook is a POST carrying { event, job } — render.completed or render.failed. Set a WEBHOOK_SIGNING_SECRET and each delivery is signed with an X-Rendral-Signature (HMAC-SHA256) header you can verify. A failed delivery is retried up to 3× with exponential backoff; every attempt carries an X-Rendral-Delivery-Attempt header.
Batch & signed URLs
Fire up to 20 renders in one call — each runs as a background job, so you get an id per item to poll. Then mint a signed URL to hand a finished result straight to a browser or teammate, no API key required. Both need an account key; signed URLs need URL_SIGNING_SECRET set on the server.
# Submit a batch → 202 with one job per item
POST /api/v1/batch
{ "requests": [
{ "type": "pdf", "template": "invoice", "data": { "id": 1 } },
{ "type": "screenshot", "url": "https://example.com" }
] }
→ { "count": 2, "accepted": 2, "jobs": [
{ "index": 0, "id": "job_ab12…", "poll_url": ".../api/v1/jobs/job_ab12…" }, … ] }
# Once a job is done, mint a time-limited public link
POST /api/v1/jobs/job_ab12…/signed-url
{ "ttl": 3600 }
→ { "url": "https://rendral.com/api/v1/jobs/job_ab12…/output?expires=…&signature=…",
"expires_at": 1750000000 }A completion webhook carries a signed output_url automatically when URL_SIGNING_SECRET is set, so consumers can download without credentials.
API key scopes
Restrict a key to only what it needs. In the dashboard, give a key one or more scopes — render (pdf/screenshot/og), pdftools (merge, watermark, pages, split, info), templates, or jobs(batch + job polling / output / signed URLs). A request outside a key's scopes returns 403 insufficient_scope. Keys created without scopes stay unrestricted.
Usage & limits
Check your consumption programmatically — quota, month-to-date usage, when the window resets, your per-minute rate limit, and whether over-quota use is billed or hard-capped.
GET /api/v1/usage
→ { "plan": "starter", "quota": 5000, "used": 1240, "remaining": 3760,
"period": { "resets_at": 1751328000000 },
"rate_limit_per_minute": 120,
"overage": { "enabled": true, "rate_per_doc_cents": 0.6 } }Batch, signed URLs, and scoped API keys are premium features — on Free/Starter they return 402 upgrade_required and unlock on Growth and above.
Health checks
GET /healthz is a cheap liveness ping (200 while the process is up). GET /api/health reports readiness — it checks the database and browser pool, returning 200 when serving and 503 when degraded. Add ?deep=1 to actively probe Chrome.
GET /api/health
{ "status": "ok", "version": "1.1.0", "uptime_s": 84213,
"db": "up", "browser": "up", "pool": { "active": 1, "max": 3 } }Caching
Identical requests are cached for an hour and served instantly — responses carry an X-Cache: HIT or MISS header, plus X-Render-Time and a Server-Timing entry. Pass "cache": false in options to force a fresh render.
Client libraries
Official SDKs wrap every endpoint — sync renders, templates, and async jobs — and return the raw bytes.
// Node — npm install @rendral/sdk
import { RenderClient } from "@rendral/sdk";
const client = new RenderClient(process.env.RENDER_API_KEY);
const pdf = await client.pdf({ template: "invoice", data: { amount: "$1,240" } });
// Background render, then wait for it:
const { id } = await client.pdfAsync({ url: "https://big.example.com" });
const job = await client.waitForJob(id);
const bytes = await client.jobOutput(job.id);# Python — pip install rendral
from rendral import RenderClient
client = RenderClient(os.environ["RENDER_API_KEY"])
pdf = client.pdf(template="invoice", data={"amount": "$1,240"})
job = client.pdf_async(url="https://big.example.com")
client.wait_for_job(job["id"])
data = client.job_output(job["id"])Any language works over plain HTTP — for example Go and Ruby:
// Go
body, _ := json.Marshal(map[string]any{"html": "<h1>Hi</h1>"})
req, _ := http.NewRequest("POST", "https://rendral.com/api/v1/pdf", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer sk_live_...")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
pdf, _ := io.ReadAll(resp.Body)# Ruby
require "net/http"; require "json"
uri = URI("https://rendral.com/api/v1/pdf")
req = Net::HTTP::Post.new(uri,
"Authorization" => "Bearer sk_live_...",
"Content-Type" => "application/json")
req.body = { html: "<h1>Hi</h1>" }.to_json
pdf = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.bodyIdempotency
Send an Idempotency-Key header to make retries safe — a repeated request returns the original response (with Idempotency-Replayed: true) and doesn't count against your quota. Keys are remembered for 24 hours.
curl https://rendral.com/api/v1/pdf \
-H "Authorization: Bearer sk_live_..." \
-H "Idempotency-Key: order-1042-invoice" \
-H "Content-Type: application/json" \
-d '{ "html": "<h1>Invoice</h1>" }'OpenAPI, Postman & agents
The full machine-readable spec lives at /openapi.json — point a client generator or an AI coding assistant at it, or import the ready-made collection at /postman.json (set the apiKey variable and Send). There's also an /llms.txt summary written for agents.
Errors
Errors return JSON with a machine-readable code and a request_id (also in the X-Request-Id header — quote it when asking for support).
{ "error": "Invalid API key.", "code": "invalid_key", "request_id": "a1ab9015-…" }Codes: missing_auth / invalid_key (401), forbidden (403), invalid_request (400), blocked_url (400), template_not_found (404), job_not_found (404), rate_limited (429), quota_exceeded (402), render_failed (500).
Rendering runs on headless Chrome. URL rendering refuses internal / private hosts.