Documentation
Everything you need to render HTML to pixels.
Authentication
All /render endpoints require a Bearer token:
curl -X POST https://www.supaserv.io/render \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"html": "<h1>Hello</h1>"}'
Keys are issued per client and held as environment variables on the gateway. There is no sign-up and no dashboard. Each request is logged with the name of the key it came in with, and a key is withdrawn by deleting that one variable. Keep it secret, it authenticates every request.
POST /render
Render HTML to a PNG screenshot.
Request Body
| Field | Type | Default | Description |
|---|---|---|---|
| html | string | - | HTML to render (required) |
| width | number | 1080 | Viewport width (100–3840) |
| height | number | 1350 | Viewport height (100–3840) |
| seek_ms | number | - | Wait ms before capture (for animations) |
| wait_ready | boolean | true | Wait for window.__READY |
Example
curl -X POST https://www.supaserv.io/render \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "html": "<div style=\"background:#000;color:#fff;padding:40px\">Hello World</div>", "width": 1080, "height": 1080 }' \ --output screenshot.png
Returns: image/png
POST /render/pdf v1 · Chromium
Render HTML to a PDF document using the Chromium engine. Also available as /render/pdfv1.
Request Body
| Field | Type | Default | Description |
|---|---|---|---|
| html | string | - | HTML to render (required) |
| format | string | "A4" | Page format (A4, A3, Letter, Legal) |
| landscape | boolean | false | Landscape orientation |
| margin | object | - | Page margins {top, right, bottom, left} |
| print_background | boolean | true | Include background colors/images |
Example
curl -X POST https://www.supaserv.io/render/pdf \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "html": "<h1>Invoice #42</h1><p>Total: EUR 99.00</p>", "format": "A4", "print_background": true }' \ --output invoice.pdf
Returns: application/pdf
POST /render/pdfv2 v2 · WeasyPrint
Print-ready PDF rendering with CMYK, ICC profiles, bleed, trim marks, and PDF/X-4 support. Powered by WeasyPrint.
Full CSS Paged Media support. Falls back to Chromium if WeasyPrint is unavailable.
Quick Example
curl -X POST https://www.supaserv.io/render/pdfv2 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "html": "<h1>Print Flyer</h1>", "format": "A4", "color_mode": "cmyk", "bleed": "3mm", "trim_marks": true }' \ --output flyer-print.pdf
Returns: application/pdf
POST /v1/pdf v1 · contract
The versioned, stateless render contract. Every asset a document needs — fonts, images, the ICC profile — is passed inline as base64. The one exception is a colour profile supaserv ships itself: that one you name (icc). The renderer fetches nothing from the network. A reference it cannot resolve is an error, never a silent substitution.
Request Body
| Field | Type | Default | Description |
|---|---|---|---|
| html | string | - | Complete document (required). Page geometry comes from @page CSS only. |
| profile | string | "screen" | screen = RGB PDF 1.7. print = PDF/X-4, DeviceCMYK, ISO Coated v2 output intent, bleed and crop marks. |
| assets | array | [] | Objects { name, media_type, data }, data is standard base64. name must match the reference in the HTML byte for byte. |
| crop_marks | boolean | true | print only. Crop and registration marks outside the bleed. |
| icc | string | - | print only. Name of an ICC profile supaserv ships. Today exactly one: iso-coated-v2-300 = ISO Coated v2 300% (ECI), sha256 c6b4b62f…cdab8. Takes the place of the inline ICC asset, never together with one. |
| output_intent | string | from icc, else "ISO Coated v2 300% (ECI)" | print only. The /OutputConditionIdentifier written into the PDF. With icc it is that profile's identifier, and any other value is 400 unsupported. With an inline ICC asset it must describe the asset you sent; supaserv does not verify the two agree. |
Fail closed: profile: "print" needs exactly one ICC profile: icc by name, or exactly one asset with media_type: "application/vnd.iccprofile". Neither, two inline, unreadable or not CMYK returns render_failed and no PDF. It never ships a file that claims PDF/X-4 without carrying the profile.
Colour is not converted. supaserv embeds the output intent, it does not transform your values. Send device-cmyk() in the CSS and CMYK images in the assets.
POST /v1/image
Same contract, same assets, same errors. One viewport per request, PNG, JPEG or WebP.
| Field | Type | Default | Description |
|---|---|---|---|
| html | string | - | Complete document (required) |
| width | number | - | CSS pixels (required) |
| height | number | - | CSS pixels. Omit for a full-page capture. |
| format | string | "png" | png, jpeg or webp |
| scale | number | 1 | Device pixel ratio, 1–4. Output is width × scale. |
| transparent | boolean | false | png and webp only |
| quality | number | 85 | jpeg and webp only |
| assets | array | [] | As above |
Errors
A closed list. Nothing else is returned.
| Code | Status | Trigger |
|---|---|---|
| auth | 401 | Missing or unknown bearer token |
| unsupported | 400 | Unknown field, bad value, or a reference the renderer is not allowed to fetch (any absolute URL, any <base>). /v1/pdf: an icc name not in the list (the message lists the names that exist), icc without profile: "print", icc together with an inline ICC asset, output_intent that contradicts icc. |
| asset_missing | 400 | The HTML references an asset that is not in assets |
| too_large | 413 | Body over 4 MB at the gateway, 32 MB at the worker |
| rate_limited | 429 | Per key and path, per minute |
| render_failed | 500 / 504 | The renderer could not produce the file. 504 when a stage ran out of time. |
Response headers
X-Engine, X-Engine-Version, X-Duration-Ms, X-Pages (pdf), X-Fonts-Embedded, X-ICC-SHA256 (pdf print: sha256 of the embedded profile, named or inline).
Requests over 4 MB must go to the worker directly at https://api.supaserv.io; the gateway is capped by the Vercel Edge body limit. Paths, bodies and errors are identical on both hosts.
POST /render/video
Capture animated HTML as MP4 or GIF.
Request Body
| Field | Type | Default | Description |
|---|---|---|---|
| html | string | - | HTML to render (required) |
| width | number | 1080 | Viewport width (100–3840) |
| height | number | 1350 | Viewport height (100–3840) |
| fps | number | 10 | Frames per second (1–30) |
| duration_ms | number | - | Duration in ms (100–30000, required) |
| format | string | "mp4" | "mp4" or "gif" |
Example
curl -X POST https://www.supaserv.io/render/video \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "html": "<div id=\"anim\">...</div>", "width": 1080, "height": 1920, "fps": 30, "duration_ms": 5000, "format": "mp4" }' \ --output animation.mp4
Returns: video/mp4 or image/gif
Limit: max 900 frames total (fps × duration). Reduce FPS or duration if exceeded.
POST /render/youtube off by default
Disabled in production. The route and yt-dlp stay in the image, but the worker answers 503 {"error":"route disabled"} unless YOUTUBE_ENABLED=true is set. The same handler serves /render/twitter and /render/x, so all three are off together. Everything below describes the enabled behaviour.
Download a video as MP4 from YouTube or any of ~1000 sites supported by yt-dlp (Vimeo, TikTok, Twitter/X, Twitch, …).
Request
| Field | Type | Description |
|---|---|---|
| url | string | Required. Video URL (https only; private/loopback hosts rejected). |
| format | string | One of best (default), 2160p, 1440p, 1080p, 720p, 480p, 360p, audio, or a raw yt-dlp format string (e.g. 137+140). |
List available formats
Add ?info=true to inspect metadata + every available stream without downloading:
curl -X POST "https://www.supaserv.io/render/youtube?info=true" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://youtu.be/oc6RV5c1yd0"}'
Returns application/json: { id, title, uploader, duration_s, thumbnail, formats: […] }
Download
curl -X POST https://www.supaserv.io/render/youtube \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://youtu.be/oc6RV5c1yd0","format":"1080p"}' \ --output video.mp4
Returns: video/mp4 (or audio/mp4 for format=audio).
Limits & long videos
Default max duration: 30 minutes (worker-side, overridable via YTDLP_MAX_DURATION_S env).
Videos longer than 3 minutes return 503 with a worker_endpoint hint. Call the worker directly to bypass the gateway timeout:
curl -X POST https://api.supaserv.io/youtube \ -H "Authorization: Bearer WORKER_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"...","format":"best"}' \ --output video.mp4
Note: respect each platform’s ToS and applicable copyright. Self-hosted means you own the compliance choice.
POST /render/rembg NEW
Remove background from an image. Returns a transparent PNG. Powered by rembg (u2net model) with edge refinement.
Best suited for images with white or light backgrounds (product photos, headshots, logos).
Request
Send as multipart/form-data with a file field. Tune aggressiveness with the optional X-Force header (Low / Medium / High):
| Field | Type | Description |
|---|---|---|
| file | File | Image file (required). JPG, PNG, or WEBP. Max 10 MB. |
| X-Force | header | Cut strength: Low, Medium (default), or High. Higher = more aggressive matting. |
Example
curl -X POST https://www.supaserv.io/render/rembg \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@photo.jpg" \ --output cutout.png
Returns: image/png (RGBA with transparent background)
Errors
| Status | Reason |
|---|---|
| 400 | Missing file, wrong Content-Type, or unsupported image format |
| 413 | File exceeds 10 MB limit |
| 500 | Processing error |
Performance: ~3s per image (warm). First request after deploy may take longer.
POST /render/pdf2text NEW
Extract text from a PDF, or let Claude read it and hand back structured JSON. Text is pulled with PyMuPDF; optional flags add vision OCR and document intelligence.
Request
Send as multipart/form-data with a file field. Behaviour is controlled by query flags:
| Flag | Type | Description |
|---|---|---|
| file | File | PDF file (required). Max 20 MB. |
| ?analyze=true | boolean | Detect document type & return typed JSON (invoice, contract, letter, report, cv, form, …). Cheap, text-only. |
| ?llm=true | boolean | Vision OCR fallback for scanned / image-only PDFs (no text layer). |
| ?visual=true | boolean | Render pages & run full Claude vision analysis. |
Example: invoice to JSON
curl -X POST "https://www.supaserv.io/render/pdf2text?analyze=true" \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@invoice.pdf"
Returns: application/json
// with ?analyze=true: detected fields live under "document" { "method": "analyze", "summary": "...", "pages": 1, "document": { "type": "invoice", "language": "de", "confidence": 0.98, "invoice_number": "2026-0420", "total": 678.30, "currency": "EUR", "line_items": [ /* description, quantity, unit_price, total, vat_rate */ ], "tax_details": [ /* rate, base, amount */ ] } }
Flags also work as headers (X-Analyze, X-Llm, X-Visual). Without any flag it returns plain extracted text + page count. Analysis uses Claude via the worker, rate limited to 10/min.
POST /render/batch
Render one animated document as a sequence of base64 PNG frames, returned as JSON instead of an encoded video. Use it when you want to do your own encoding, build a sprite sheet, or inspect single frames.
Request Body
| Field | Type | Default | Description |
|---|---|---|---|
| html | string | - | HTML to render (required) |
| width | number | 1080 | Viewport width (100–3840) |
| height | number | 1350 | Viewport height (100–3840) |
| fps | number | 10 | Frames per second (1–30) |
| duration_ms | number | - | Duration in ms (100–30000, required) |
| concurrency | number | 4 | Frames rendered in parallel (1–6) |
Example
curl -X POST https://www.supaserv.io/render/batch \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"html":"<div id=\"anim\">...</div>","fps":10,"duration_ms":3000}'
Returns: application/json
{
"frames": [ /* base64 PNG, one per frame */ ],
"fps": 10,
"duration_ms": 3000,
"width": 1080,
"height": 1350,
"total_frames": 30
}
Limit: max 300 frames (fps × duration). The response is JSON, so it is far larger than the equivalent MP4 — use /render/video unless you need the individual frames.
GET /health
Health check endpoint. No authentication required.
curl https://www.supaserv.io/health
// Response { "status": "ok", "service": "supaserv", "version": "0.2.0", "backend": "worker" }