Official Python SDK for Rendex — render HTML or Markdown to images, generate PDFs, capture screenshots, monitor pages for changes with Rendex Watch, mint hosted render links, and extract clean Markdown/JSON/HTML from URLs
Project description
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— 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rendex-1.5.2.tar.gz.
File metadata
- Download URL: rendex-1.5.2.tar.gz
- Upload date:
- Size: 32.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"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.0"},"openssl_version":"OpenSSL 3.0.13 30 Jan 2024","python":"3.12.13","system":{"name":"Linux","release":"6.17.0-1018-azure"}} HTTPX2/2.4.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
196700782210ba2501c36e44c8efe7132748b6082450a9510d3c5f36858973c5
|
|
| MD5 |
9a5e2ef926e4f218ba43e65081ea88af
|
|
| BLAKE2b-256 |
c320202eb30dc8e8a8058020a2270c6b566dabb30292dbb8ee80d8eeeb597d1d
|
File details
Details for the file rendex-1.5.2-py3-none-any.whl.
File metadata
- Download URL: rendex-1.5.2-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"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.0"},"openssl_version":"OpenSSL 3.0.13 30 Jan 2024","python":"3.12.13","system":{"name":"Linux","release":"6.17.0-1018-azure"}} HTTPX2/2.4.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db7a2eab1096fae1e7944c94831a5cbf9399626314a9642fc267542f6dc31483
|
|
| MD5 |
0d791bb7b180619e5769feb2e07cb14f
|
|
| BLAKE2b-256 |
aebe01535f0be32156f6c4e8a9d45d6b24491d53aa2973fb710a0f3f054844be
|