rendex
Official Python SDK for Rendex — render HTML, a URL, or Markdown to an image or PDF with one call.
Common job: render HTML charts, financial tables, and report sections to high-resolution PNG from a backend script, then embed them in the PDF or report your app generates — no headless browser to install.
# pip install rendex
from rendex import Rendex
result = Rendex("YOUR_API_KEY").render_html("<table>…your report HTML…</table>", format="png")
open("table.png", "wb").write(result.image) # embed result.image in your PDF
- Skip the
requestsboilerplate — typedrender_html()returns image bytes + metadata - Full type hints (PEP 561 compatible)
- Single dependency (
httpx) - Sync API with context manager support
- Typed error handling with API error codes
Install
pip install rendex
Quick Start
from pathlib import Path
from rendex import Rendex
rendex = Rendex("your-api-key")
# Render raw HTML straight to a PNG
result = rendex.render_html("<h1>Hello, world</h1>")
Path("hello.png").write_bytes(result.image)
# Or render Markdown (converted to HTML server-side)
md = rendex.render_markdown("# Hello, world\n\nRendered from **Markdown**.")
Path("hello-md.png").write_bytes(md.image)
# Or capture a live URL
shot = rendex.screenshot("https://example.com", format="png", full_page=True)
Path("screenshot.png").write_bytes(shot.image)
print(f"{result.metadata.bytes_size} bytes, loaded in {result.metadata.load_time_ms}ms")
API Reference
Rendex(api_key, *, base_url="https://api.rendex.dev")
Create a new Rendex client.
rendex = Rendex("your-api-key")
# Or with context manager for connection reuse
with Rendex("your-api-key") as rendex:
result = rendex.screenshot("https://example.com")
rendex.screenshot(url, **options)
Capture a screenshot and return the binary image with metadata.
result = rendex.screenshot(
"https://example.com",
format="webp",
width=1920,
height=1080,
dark_mode=True,
)
Path("screenshot.webp").write_bytes(result.image)
print(result.metadata.load_time_ms) # 350
Returns ScreenshotResult:
image—bytesof the captured imagemetadata—ScreenshotMetadatawith url, dimensions, format, bytes_size, load_time_ms, quality, etc.
rendex.render_html(html, **options)
Render raw HTML straight to an image and return the binary result. Convenience wrapper over screenshot(html=...).
result = rendex.render_html("<h1>Hello, world</h1>", format="png", width=1200)
Path("hello.png").write_bytes(result.image)
Returns ScreenshotResult — same shape as screenshot().
You can also pass html directly to screenshot() / screenshot_json():
result = rendex.screenshot(html="<h1>Hello, world</h1>", full_page=True)
HTML rendering is POST-only and accepts up to 5MB. It is not available via
screenshot_url()(the GET endpoint can't carry an HTML body). Provide exactly one ofurl,html, ormarkdown; the SDK raisesRendexErrorlocally if you supply none or more than one.
rendex.render_markdown(markdown, **options)
Render Markdown straight to an image and return the binary result. The server converts the Markdown to HTML before rendering. Convenience wrapper over screenshot(markdown=...).
result = rendex.render_markdown("# Hello, world\n\nRendered from **Markdown**.", format="png", width=1200)
Path("hello.png").write_bytes(result.image)
Returns ScreenshotResult — same shape as screenshot().
You can also pass markdown directly to screenshot() / screenshot_json():
result = rendex.screenshot(markdown="# Hello, world", full_page=True)
Markdown rendering is POST-only and accepts up to 5MB. Like
html, it is not available viascreenshot_url(). Provide exactly one ofurl,html, ormarkdown.
rendex.screenshot_json(url, **options)
Capture a screenshot and return JSON with a base64-encoded image.
result = rendex.screenshot_json("https://example.com")
print(result["data"]["bytesSize"]) # 45823
print(result["meta"]["usage"]["remaining"]) # 499
Returns ScreenshotJsonResponse dict with data (image + metadata) and meta (request ID, usage).
rendex.render_html_json(html, **options)
Render raw HTML and return JSON with a base64-encoded image. Convenience wrapper over screenshot_json(html=...).
result = rendex.render_html_json("<h1>Invoice #1042</h1>", format="pdf", pdf_format="Letter")
print(result["data"]["format"]) # "pdf"
rendex.render_markdown_json(markdown, **options)
Render Markdown and return JSON with a base64-encoded image. The server converts the Markdown to HTML before rendering. Convenience wrapper over screenshot_json(markdown=...).
result = rendex.render_markdown_json("# Invoice #1042", format="pdf", pdf_format="Letter")
print(result["data"]["format"]) # "pdf"
Mustache data templating
Pass a data dict alongside html or markdown to render logic-less Mustache templates before capture. This lets you generate invoices, reports, or any dynamic content without string-formatting Python-side.
from pathlib import Path
from rendex import Rendex
rendex = Rendex("your-api-key")
# Markdown invoice with variable substitution and a loop
template = """
# Invoice \#{{number}}
| Item | Qty | Price |
|------|-----|-------|
{{#items}}
| {{name}} | {{qty}} | {{price}} |
{{/items}}
**Total: {{total}}**
"""
result = rendex.render_markdown(
template,
data={
"number": "1042",
"items": [
{"name": "Screenshot API — Pro", "qty": 1, "price": "€ 49.00"},
{"name": "Priority support", "qty": 1, "price": "€ 5.00"},
],
"total": "€ 54.00",
},
format="pdf",
pdf_format="A4",
)
Path("invoice-1042.pdf").write_bytes(result.image)
The same data parameter works on render_html, screenshot, screenshot_json, render_html_json, and render_markdown_json.
Template syntax (server-side, logic-less Mustache):
| Syntax | Behaviour |
|---|---|
{{var}} |
HTML-escaped substitution |
{{{var}}} |
Raw (unescaped) substitution |
{{#section}}...{{/section}} |
Render block for each item in a list (or once if truthy) |
{{^section}}...{{/section}} |
Render block when section is falsy / empty |
{{a.b}} |
Nested property access |
datais not valid withurl(the server returns 400). Serialized payload is capped at 256 KB server-side.
rendex.screenshot_url(url, **options)
Generate a GET URL for embedding. No network call — pure URL builder.
url = rendex.screenshot_url("https://example.com", format="png", width=1200)
# Use in <img> tags, OpenGraph, etc.
Note: The API key is included in the URL. Use server-side only.
Async jobs & batches
For long-running captures or many URLs at once, submit work asynchronously and poll for results.
# Single async job: returns immediately, poll for the result
job = rendex.screenshot_json("https://example.com", async_mode=True)
job_id = job["data"]["jobId"]
status = rendex.job_status(job_id)
if status["data"]["status"] == "completed":
print(status["data"]["resultUrl"])
# Batch: 1–500 URLs with shared defaults
batch = rendex.batch(
["https://example.com", "https://github.com"],
defaults={"format": "webp", "full_page": True},
cache_ttl=7200,
)
batch_id = batch["data"]["batchId"]
progress = rendex.batch_status(batch_id)
print(f'{progress["data"]["completedJobs"]}/{progress["data"]["totalJobs"]} done')
rendex.batch(urls, *, defaults=None, webhook_url=None, cache_ttl=None)→BatchCreateResponserendex.job_status(job_id)→JobStatusResponserendex.batch_status(batch_id)→BatchStatusResponse
Batch capture is URL-only (raw HTML is single-request);
defaultsaccepts the same snake_case options asscreenshot().
Screenshot Options
All options are keyword arguments in snake_case. Only url (positional) is required:
| Option | Type | Default | Description |
|---|---|---|---|
format |
str |
"png" |
"png", "jpeg", "webp", or "pdf" |
width |
int |
1280 |
Viewport width (320–3840) |
height |
int |
800 |
Viewport height (240–2160) |
full_page |
bool |
False |
Capture the full scrollable page |
quality |
int |
80 |
JPEG/WebP quality (1–100, default 80) |
delay |
int |
0 |
Delay before capture in ms (0–10000) |
dark_mode |
bool |
False |
Emulate dark mode |
device_scale_factor |
float |
2 |
Device pixel ratio (1–3). 2× Retina by default |
block_ads |
bool |
True |
Block ads and trackers |
block_resource_types |
list |
— | Block: "font", "image", "media", "stylesheet", "other" |
timeout |
int |
30 |
Page load timeout in seconds (5–60) |
wait_until |
str |
"networkidle2" |
"load", "domcontentloaded", "networkidle0", "networkidle2" |
wait_for_selector |
str |
— | CSS selector to wait for |
best_attempt |
bool |
True |
Return best-effort screenshot on timeout |
selector |
str |
— | Capture a specific element by CSS selector |
css / js |
str |
— | Inject custom CSS / JavaScript before capture (max 50KB each) |
cookies / headers |
list / dict |
— | Cookies (max 50) and custom HTTP headers for the request |
user_agent |
str |
— | Override the browser user-agent string |
geo / geo_city / geo_state |
str |
— | Geo-targeted capture (Pro/Enterprise) |
pdf_format |
str |
"A4" |
PDF page size: "A4", "Letter", "Legal", "Tabloid", "A3" (format="pdf") |
pdf_landscape |
bool |
False |
PDF landscape orientation |
pdf_print_background |
bool |
True |
Print background colors/images in the PDF |
pdf_margin |
dict |
— | PDF margins, e.g. {"top": "1cm", "bottom": "1cm"} |
pdf_scale |
float |
1 |
PDF render scale (0.1–2) |
async_mode |
bool |
False |
Return a job ID immediately (sent as async) |
webhook_url |
str |
— | URL to call when an async capture completes |
cache_ttl |
int |
86400 |
Signed result URL TTL in seconds (3600–2592000) |
Rendex Watch
Monitor a URL on a schedule and get alerted when it changes — real-Chrome visual diff (with a highlighted overlay), an extracted-text diff, or both. One API key, your shared credit pool.
from rendex import Rendex
with Rendex("your-api-key") as rendex:
# Create a watch (an active watch captures its baseline immediately — 1 credit)
watch = rendex.create_watch(
"https://example.com/pricing",
diff_mode="visual", # "visual" | "text" | "both"
interval_minutes=1440, # your plan's floor is the minimum
webhook_url="https://hooks.example.com/rendex", # Starter+; HMAC-signed
notify_email="you@example.com", # any plan; your account email
)
# Dry-run a config BEFORE saving — what we'd capture + is the page reachable
test = rendex.test_watch("https://example.com", render_params={"selector": "#price"})
if test["reachable"]:
print(test["screenshotUrl"])
rendex.list_watches(status="active") # {"items": [...], "nextCursor": ...}
rendex.get_watch(watch["id"])
rendex.run_watch(watch["id"]) # run a check now → {"runId": ...}
runs = rendex.list_watch_runs(watch["id"], limit=10)
rendex.update_watch(watch["id"], paused=True) # pause; paused=False resumes; webhook_url=None clears it
rendex.delete_watch(watch["id"])
| Method | Endpoint | Returns |
|---|---|---|
create_watch(url, **opts) |
POST /v1/watches |
Watch |
list_watches(*, status, cursor, limit) |
GET /v1/watches |
{"items": [...], "nextCursor"} |
get_watch(watch_id) |
GET /v1/watches/:id |
Watch |
update_watch(watch_id, **patch) |
PATCH /v1/watches/:id |
Watch |
delete_watch(watch_id) |
DELETE /v1/watches/:id |
None |
run_watch(watch_id) |
POST /v1/watches/:id/run |
{"runId", "watchId", "status"} |
list_watch_runs(watch_id, *, cursor, limit) |
GET /v1/watches/:id/runs |
{"items": [...], "nextCursor"} |
test_watch(url, **opts) |
POST /v1/watches/test |
WatchTestResult |
Render knobs (full_page, selector, ignore_regions, ignore_text, ua_mode, …) go under render_params= (snake_case, camelCased on send). To clear an alert channel on update, pass it explicitly as None (e.g. update_watch(id, webhook_url=None)).
Verifying Watch webhooks
When a watched page changes Rendex POSTs an HMAC-signed watch.changed (or watch.recovered / watch.error) event to your webhook_url. Verify it with the raw request body:
from rendex import verify_webhook, RendexWebhookPayload
import json, os
# raw_body is the unparsed request body string
ok = verify_webhook(
raw_body,
request.headers["x-rendex-signature"],
request.headers["x-rendex-timestamp"],
os.environ["WEBHOOK_SIGNING_SECRET"],
)
if not ok:
abort(400)
event: RendexWebhookPayload = json.loads(raw_body)
if event["event"] == "watch.changed":
print(event["url"], event["diffScore"], event["diffOverlayUrl"])
A change event uses one vocabulary everywhere:
diffScore,diffPixels,beforeUrl,afterUrl,diffOverlayUrl,cropUrl(a tight crop of just the changed region),changedRegion(its normalized 0–1 coordinates), andsummary(a one-line plain-English "what changed" sentence, on every plan) — identical on the webhook payload and on a RESTWatchRun.
Error Handling
from rendex import Rendex, RendexApiError, RendexNetworkError
rendex = Rendex("your-api-key")
try:
rendex.screenshot("https://example.com")
except RendexApiError as e:
# API returned an error
print(e.error_code) # "RATE_LIMITED", "VALIDATION_ERROR", etc.
print(e.status_code) # 429, 400, etc.
print(e.request_id) # For debugging with Rendex support
print(e.details) # Validation details (if any)
except RendexNetworkError as e:
# Network failure (DNS, timeout, connection refused)
print(f"Network error: {e}")
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Invalid request parameters |
INVALID_URL |
400 | URL failed SSRF validation |
TIMEOUT |
408 | Page took too long to load |
CAPTURE_FAILED |
500 | Browser rendering error |
RATE_LIMITED |
429 | Rate limit exceeded |
USAGE_EXCEEDED |
429 | Monthly credit limit reached |
MISSING_API_KEY |
401 | No API key provided |
INVALID_API_KEY |
401 | API key verification failed |
License
MIT - Copperline Labs LLC
Metadata
Release files for rendex 1.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rendex-1.8.0.tar.gz | 34.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rendex-1.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.6 kB
Release files / rendex-1.8.0.tar.gz
| Download URL | rendex-1.8.0.tar.gz |
|---|---|
| Size | 34.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
627b9f6f980926a2ab97a0e089094d7bf4eabee7cf565540b5f310bc47a38cd5
|
|
BLAKE2b-256 checksum How to use checksums |
9245b27d498c1c3efafa1965d5e3fff5b3bb812865da1f164ac8b181971fc75c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
Hatch/1.17.1 {"ci":true,"cpu":"x86_64","distro":{"id":"noble","libc":{"lib":"glibc","version":"2.39"},"name":"Ubuntu","version":"24.04"},"implementation":{"name":"CPython","version":"3.12.13"},"installer":{"name":"hatch","version":"1.17.1"},"openssl_version":"OpenSSL 3.0.13 30 Jan 2024","python":"3.12.13","system":{"name":"Linux","release":"6.17.0-1020-azure"}} HTTPX2/2.7.0
|
Release files / rendex-1.8.0-py3-none-any.whl
| Download URL | rendex-1.8.0-py3-none-any.whl |
|---|---|
| Size | 24.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e2ed4905ada011719dd3172d8656af3d2afdd19701c2d5b54c86222132a9c407
|
|
BLAKE2b-256 checksum How to use checksums |
4142ad22866362dd87aad3cd51e151199e53715953fae0316ef6e6fde07c838b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
Hatch/1.17.1 {"ci":true,"cpu":"x86_64","distro":{"id":"noble","libc":{"lib":"glibc","version":"2.39"},"name":"Ubuntu","version":"24.04"},"implementation":{"name":"CPython","version":"3.12.13"},"installer":{"name":"hatch","version":"1.17.1"},"openssl_version":"OpenSSL 3.0.13 30 Jan 2024","python":"3.12.13","system":{"name":"Linux","release":"6.17.0-1020-azure"}} HTTPX2/2.7.0
|