RenderBadger API
Two endpoints, one key. Base URL https://renderbadger.sh. All request bodies are JSON.
Quickstart
- Sign in and create an API key on the dashboard.
- Render your first PDF:
curl -X POST https://renderbadger.sh/v1/pdf \
-H "Authorization: Bearer $RENDERBADGER_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Invoice #42</h1><p>Total: €120</p>","format":"A4","margin":{"top":"20mm","bottom":"20mm"}}' \
-o invoice.pdf
The response body is the PDF. Headers carry the job id and a signed URL you can hand to a browser:
x-renderbadger-job: job_... x-renderbadger-url: https://renderbadger.sh/o/acct_.../job_....pdf?e=...&s=... x-renderbadger-expires: 2026-10-27T12:00:00.000Z x-renderbadger-duration-ms: 1830
Prefer JSON? Add "output": "url" and you get the job object (see jobs) instead of the file.
// Node 18+, no SDK needed
const res = await fetch("https://renderbadger.sh/v1/screenshot", {
method: "POST",
headers: { authorization: `Bearer ${process.env.RENDERBADGER_KEY}`, "content-type": "application/json" },
body: JSON.stringify({ url: "https://example.com", fullPage: true, output: "url" }),
});
const job = await res.json(); // { id, status: "done", url, bytes, ... }
Authentication
Send your key as a bearer token: Authorization: Bearer rb_live_.... Keys are shown once when created and can be revoked from the dashboard. Revocation takes effect within five minutes everywhere.
POST /v1/pdf
Renders a URL or an HTML string to PDF with Chrome's print engine. Media type defaults to print.
| Field | Type | Default | Notes |
|---|---|---|---|
| format | string | A4 | Letter, Legal, Tabloid, Ledger, A0–A6 |
| width, height | CSS length | Custom page size, e.g. "210mm". Overrides format when both set. | |
| landscape | boolean | false | |
| margin | object | {} | {top,right,bottom,left} as CSS lengths |
| printBackground | boolean | true | |
| scale | number | 1 | 0.1–2 |
| pageRanges | string | e.g. "1-3,5" | |
| preferCSSPageSize | boolean | false | Honour @page { size } |
| headerTemplate, footerTemplate | HTML | Chrome templates; use classes pageNumber, totalPages, date, title, url. Set a top/bottom margin so they have room. |
POST /v1/screenshot
Renders a URL or HTML to PNG, JPEG or WebP. Media type defaults to screen.
| Field | Type | Default | Notes |
|---|---|---|---|
| type | string | png | png, jpeg, webp |
| quality | integer | 1–100, jpeg and webp only | |
| fullPage | boolean | false | |
| selector | string | Screenshot one element | |
| clip | object | {x,y,width,height} in CSS px | |
| omitBackground | boolean | false | Transparent PNG background |
Common options
| Field | Type | Default | Notes |
|---|---|---|---|
| url | string | Exactly one of url or html. Public http(s) only. | |
| html | string | Up to 5 MB. Relative URLs inside need a <base href>. | |
| viewport | object | 1280×800 | {width,height,deviceScaleFactor,isMobile} |
| waitFor | string | CSS selector that must be visible before rendering | |
| waitUntil | string | networkidle0 | load, domcontentloaded, networkidle0, networkidle2 |
| delay | ms | 0 | Extra wait after the page settles, up to 10 s |
| cookies | array | [] | {name,value,domain?,path?} |
| headers | object | {} | Extra request headers sent by the browser |
| auth | object | HTTP basic auth {username,password} | |
| userAgent | string | ||
| emulateMedia | string | print / screen | screen or print |
| timeout | ms | 20000 | Whole render, 1–30 s |
| output | string | binary | binary streams the file, url returns the job JSON |
| async | boolean | false | Queue the render, respond 202 (Starter and up) |
| webhookUrl | string | POSTed when an async job finishes |
Async jobs and webhooks
With "async": true the API responds 202 with a job object and a Location header. Poll GET /v1/jobs/:id or set webhookUrl. The webhook body is { "event": "job.completed" | "job.failed", "job": {...} }, retried with backoff for up to eight attempts until your endpoint returns 2xx.
Verify the RenderBadger-Signature: t=<unix>,v1=<hex> header: compute HMAC-SHA256 of "<t>.<raw body>" with your webhook secret from the dashboard and compare to v1.
GET /v1/jobs/:id
{
"id": "job_...",
"kind": "pdf",
"status": "queued" | "running" | "done" | "failed",
"url": "https://renderbadger.sh/o/...", // signed, null until done
"contentType": "application/pdf",
"bytes": 48213,
"durationMs": 1830,
"expiresAt": "2026-11-02T10:00:00.000Z",
"error": null | { "code": "render_timeout", "message": "..." },
"createdAt": "...", "finishedAt": "..."
}
Outputs are kept 24 h (Free), 7 d (Starter) or 30 d (Pro and Scale), then purged. Add ?download to a signed URL to force a download.
Idempotency
Send an Idempotency-Key header (any string up to 200 chars) and a retry with the same key and body returns the original result instead of rendering again. A different body with the same key returns 409 idempotency_conflict.
Limits
| Plan | Renders / mo | Concurrent | Requests / min | Retention | Async |
|---|---|---|---|---|---|
| Free | 100 (hard cap) | 1 | 10 | 24 h | no |
| Starter | 2,000 | 2 | 60 | 7 d | yes |
| Pro | 10,000 | 5 | 300 | 30 d | yes |
| Scale | 50,000 | 10 | 1,000 | 30 d | yes |
Over the concurrency cap you get 429 concurrency_limited with a Retry-After header; async requests are queued instead and never hit this.
Error codes
Errors are JSON: { "error": { "code", "message", "details?" } }.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Body is not JSON or fails validation; details lists the fields |
| 401 | unauthorized | Missing, malformed or revoked key |
| 402 | payment_required | Feature needs a higher plan (e.g. async on Free) |
| 402 | quota_exceeded | Free plan monthly cap reached |
| 404 | not_found | Unknown job or endpoint |
| 409 | idempotency_conflict | Same Idempotency-Key, different body |
| 422 | blocked_url | Private, local or denylisted host |
| 429 | rate_limited | Per-minute request limit; see Retry-After |
| 429 | concurrency_limited | Too many renders in flight |
| 502 | navigation_failed | The target URL could not be loaded |
| 502 | render_failed | Chrome failed to produce output |
| 504 | render_timeout | The render exceeded timeout |
| 500 | internal | Our fault; the job id is in details, tell us |