RenderBadger API

Two endpoints, one key. Base URL https://renderbadger.sh. All request bodies are JSON.

Quickstart

  1. Sign in and create an API key on the dashboard.
  2. 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.

FieldTypeDefaultNotes
formatstringA4Letter, Legal, Tabloid, Ledger, A0–A6
width, heightCSS lengthCustom page size, e.g. "210mm". Overrides format when both set.
landscapebooleanfalse
marginobject{}{top,right,bottom,left} as CSS lengths
printBackgroundbooleantrue
scalenumber10.1–2
pageRangesstringe.g. "1-3,5"
preferCSSPageSizebooleanfalseHonour @page { size }
headerTemplate, footerTemplateHTMLChrome 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.

FieldTypeDefaultNotes
typestringpngpng, jpeg, webp
qualityinteger1–100, jpeg and webp only
fullPagebooleanfalse
selectorstringScreenshot one element
clipobject{x,y,width,height} in CSS px
omitBackgroundbooleanfalseTransparent PNG background

Common options

FieldTypeDefaultNotes
urlstringExactly one of url or html. Public http(s) only.
htmlstringUp to 5 MB. Relative URLs inside need a <base href>.
viewportobject1280×800{width,height,deviceScaleFactor,isMobile}
waitForstringCSS selector that must be visible before rendering
waitUntilstringnetworkidle0load, domcontentloaded, networkidle0, networkidle2
delayms0Extra wait after the page settles, up to 10 s
cookiesarray[]{name,value,domain?,path?}
headersobject{}Extra request headers sent by the browser
authobjectHTTP basic auth {username,password}
userAgentstring
emulateMediastringprint / screenscreen or print
timeoutms20000Whole render, 1–30 s
outputstringbinarybinary streams the file, url returns the job JSON
asyncbooleanfalseQueue the render, respond 202 (Starter and up)
webhookUrlstringPOSTed 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

PlanRenders / moConcurrentRequests / minRetentionAsync
Free100 (hard cap)11024 hno
Starter2,0002607 dyes
Pro10,000530030 dyes
Scale50,000101,00030 dyes

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?" } }.

HTTPCodeMeaning
400invalid_requestBody is not JSON or fails validation; details lists the fields
401unauthorizedMissing, malformed or revoked key
402payment_requiredFeature needs a higher plan (e.g. async on Free)
402quota_exceededFree plan monthly cap reached
404not_foundUnknown job or endpoint
409idempotency_conflictSame Idempotency-Key, different body
422blocked_urlPrivate, local or denylisted host
429rate_limitedPer-minute request limit; see Retry-After
429concurrency_limitedToo many renders in flight
502navigation_failedThe target URL could not be loaded
502render_failedChrome failed to produce output
504render_timeoutThe render exceeded timeout
500internalOur fault; the job id is in details, tell us