Skip to main content

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 requests boilerplate — typed render_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:

  • imagebytes of the captured image
  • metadataScreenshotMetadata with 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 of url, html, or markdown; the SDK raises RendexError locally 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 via screenshot_url(). Provide exactly one of url, html, or markdown.

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

data is not valid with url (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)BatchCreateResponse
  • rendex.job_status(job_id)JobStatusResponse
  • rendex.batch_status(batch_id)BatchStatusResponse

Batch capture is URL-only (raw HTML is single-request); defaults accepts the same snake_case options as screenshot().

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), and summary (a one-line plain-English "what changed" sentence, on every plan) — identical on the webhook payload and on a REST WatchRun.

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rendex-1.8.0.tar.gz (34.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rendex-1.8.0-py3-none-any.whl (24.6 kB view details)

Uploaded Python 3

File details

Details for the file rendex-1.8.0.tar.gz.

File metadata

  • Download URL: rendex-1.8.0.tar.gz
  • Upload date:
  • Size: 34.0 kB
  • Tags: Source
  • Uploaded using 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

File hashes

Hashes for rendex-1.8.0.tar.gz
Algorithm Hash digest
SHA256 627b9f6f980926a2ab97a0e089094d7bf4eabee7cf565540b5f310bc47a38cd5
MD5 c7d522440029655b3f859df4841abd3c
BLAKE2b-256 9245b27d498c1c3efafa1965d5e3fff5b3bb812865da1f164ac8b181971fc75c

See more details on using hashes here.

File details

Details for the file rendex-1.8.0-py3-none-any.whl.

File metadata

  • Download URL: rendex-1.8.0-py3-none-any.whl
  • Upload date:
  • Size: 24.6 kB
  • Tags: Python 3
  • Uploaded using 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

File hashes

Hashes for rendex-1.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e2ed4905ada011719dd3172d8656af3d2afdd19701c2d5b54c86222132a9c407
MD5 843c91bbb68c0d796eb6fa01e34408dc
BLAKE2b-256 4142ad22866362dd87aad3cd51e151199e53715953fae0316ef6e6fde07c838b

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.8.0 This release

2 files

1.6.0

2 files

1.5.2

2 files

1.5.1

2 files

1.5.0

2 files

1.4.2

2 files

1.4.1

2 files

1.4.0

2 files

1.3.0

2 files

1.1.1

2 files

1.1.0

2 files

1.0.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page