<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dan E</title>
    <description>The latest articles on DEV Community by Dan E (@danelliot).</description>
    <link>https://dev.to/danelliot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868746%2F528fa19c-77de-4c58-9929-e517022c67e3.jpeg</url>
      <title>DEV Community: Dan E</title>
      <link>https://dev.to/danelliot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danelliot"/>
    <language>en</language>
    <item>
      <title>Add Screenshot Capability to Windsurf via MCP</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 04 Sep 2026 18:48:53 +0000</pubDate>
      <link>https://dev.to/danelliot/add-screenshot-capability-to-windsurf-via-mcp-2gjp</link>
      <guid>https://dev.to/danelliot/add-screenshot-capability-to-windsurf-via-mcp-2gjp</guid>
      <description>&lt;h1&gt;
  
  
  Add Screenshot Capability to Windsurf via MCP
&lt;/h1&gt;

&lt;p&gt;Windsurf's Cascade AI can read every file in your project and run terminal commands, but it cannot see a rendered web page. The Model Context Protocol (MCP) changes that: connect the Rendex MCP server and Cascade gains a tool it can call on its own to capture any URL and describe what it sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windsurf installed (any recent version with MCP support)&lt;/li&gt;
&lt;li&gt;Node.js on your machine (for npx)&lt;/li&gt;
&lt;li&gt;A Rendex API key. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get one free&lt;/a&gt; (100 renders/month, no credit card)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Get a Rendex API key
&lt;/h2&gt;

&lt;p&gt;Sign in at &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;rendex.dev/login&lt;/a&gt;, open the dashboard, and copy your key. It starts with &lt;code&gt;rdx_&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Open the MCP Servers panel
&lt;/h2&gt;

&lt;p&gt;In Windsurf, click the gear icon in the status bar and select &lt;strong&gt;MCP Servers&lt;/strong&gt;. This opens &lt;code&gt;mcp_config.json&lt;/code&gt;, the file Cascade reads at startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add the Rendex server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rendex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@copperline/rendex-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"RENDEX_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rdx_your_key_here"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;rdx_your_key_here&lt;/code&gt; with your actual key and save the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Restart Cascade
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Restart Cascade&lt;/strong&gt; in the MCP Servers panel, or close and reopen Windsurf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify the tool is available
&lt;/h2&gt;

&lt;p&gt;In a Cascade chat, ask: &lt;em&gt;What MCP tools do you have?&lt;/em&gt; Cascade should list &lt;code&gt;rendex_screenshot&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Capture a page
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Screenshot https://rendex.dev and describe the hero section.

Capture https://example.com/checkout as a full-page PNG and check for layout issues.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cascade calls &lt;code&gt;rendex_screenshot&lt;/code&gt;, gets the rendered page back, and answers from what it sees:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b4fniul0hcfez1hx0mi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b4fniul0hcfez1hx0mi.png" alt="Windsurf Cascade chat: the user asks to screenshot stripe.com, Cascade calls rendex_screenshot, and returns the captured Stripe hero in a browser frame with a written summary of the headline and CTA" width="800" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;rendex_screenshot&lt;/code&gt; does not appear.&lt;/strong&gt; Run &lt;code&gt;npx @copperline/rendex-mcp --version&lt;/code&gt; in a terminal to confirm Node.js is installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication error.&lt;/strong&gt; Check that &lt;code&gt;RENDEX_API_KEY&lt;/code&gt; in &lt;code&gt;mcp_config.json&lt;/code&gt; matches your dashboard key exactly, including the &lt;code&gt;rdx_&lt;/code&gt; prefix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cascade times out.&lt;/strong&gt; Ask Cascade to add &lt;code&gt;blockResourceTypes: ["media", "font"]&lt;/code&gt; or use &lt;code&gt;waitUntil: "domcontentloaded"&lt;/code&gt; for faster captures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/docs/mcp" rel="noopener noreferrer"&gt;MCP documentation&lt;/a&gt; — hosted OAuth server config and full tool schema&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/blog/mcp-screenshot-cursor" rel="noopener noreferrer"&gt;Cursor MCP guide&lt;/a&gt; — same setup for Cursor (&lt;code&gt;.cursor/mcp.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;Free screenshot tool&lt;/a&gt; — test URLs before adding to Cascade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; and start with 100 captures per month.&lt;/p&gt;

</description>
      <category>windsurf</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Convert URLs to PDF in Node.js</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 28 Aug 2026 00:45:07 +0000</pubDate>
      <link>https://dev.to/danelliot/how-to-convert-urls-to-pdf-in-nodejs-4ac7</link>
      <guid>https://dev.to/danelliot/how-to-convert-urls-to-pdf-in-nodejs-4ac7</guid>
      <description>&lt;h1&gt;
  
  
  How to Convert URLs to PDF in Node.js
&lt;/h1&gt;

&lt;p&gt;Converting a URL to PDF in Node.js usually means spinning up Puppeteer, managing a Chrome process, and writing error-handling code for when the browser runs out of memory. The Rendex API handles the browser work: pass a URL, get a hosted PDF back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18 or later (or Bun)&lt;/li&gt;
&lt;li&gt;A Rendex API key. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get one free&lt;/a&gt; (100 renders/month, no credit card)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Install the SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @copperline/rendex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Capture a URL as a PDF
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RENDEX_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/invoice/1001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pdfFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pdfMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pdfPrintBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Hosted URL — share or store as-is&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Raw bytes — write to disk or stream to a storage bucket&lt;/span&gt;
&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice-1001.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Page size and margins
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;pdfFormat&lt;/code&gt; option controls the paper size. Common values: &lt;code&gt;"A4"&lt;/code&gt;, &lt;code&gt;"Letter"&lt;/code&gt;, &lt;code&gt;"Legal"&lt;/code&gt;, &lt;code&gt;"A3"&lt;/code&gt;, &lt;code&gt;"Tabloid"&lt;/code&gt;. Margins accept any CSS length unit: &lt;code&gt;px&lt;/code&gt;, &lt;code&gt;mm&lt;/code&gt;, &lt;code&gt;cm&lt;/code&gt;, or &lt;code&gt;in&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Background colors and images
&lt;/h2&gt;

&lt;p&gt;Browsers strip CSS backgrounds when printing by default. Set &lt;code&gt;pdfPrintBackground: true&lt;/code&gt; to keep brand colors and background images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Batch PDF generation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoiceUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pdfFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pdfMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15mm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;pdfPrintBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://yourapp.com/webhooks/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// batch.batchId — poll GET /v1/batches/:batchId for per-job status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Blank PDF.&lt;/strong&gt; Set &lt;code&gt;pdfPrintBackground: true&lt;/code&gt; if the page uses CSS backgrounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing images.&lt;/strong&gt; The page must be publicly accessible. Inline private assets as base64 data URIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content cut off.&lt;/strong&gt; Remove fixed heights from the root element or use a taller &lt;code&gt;pdfFormat&lt;/code&gt; like &lt;code&gt;"A3"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/tools/url-to-pdf" rel="noopener noreferrer"&gt;Free URL-to-PDF tool&lt;/a&gt; — test any URL before writing code&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; — full parameter list including &lt;code&gt;waitForSelector&lt;/code&gt; and cookies&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/blog/url-to-pdf-python" rel="noopener noreferrer"&gt;Python PDF guide&lt;/a&gt; — same guide with the Python SDK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; and start with 100 renders per month.&lt;/p&gt;

</description>
      <category>node</category>
      <category>pdf</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>SPA Screenshot: waitFor Strategies for React, Vue, Svelte</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 21 Aug 2026 16:31:30 +0000</pubDate>
      <link>https://dev.to/danelliot/spa-screenshot-waitfor-strategies-for-react-vue-svelte-1ok6</link>
      <guid>https://dev.to/danelliot/spa-screenshot-waitfor-strategies-for-react-vue-svelte-1ok6</guid>
      <description>&lt;h1&gt;
  
  
  Capturing SPAs: waitFor Strategies for React, Vue, and Svelte
&lt;/h1&gt;

&lt;p&gt;Single-page apps render content after the initial HTML loads. A screenshot taken too early captures a loading spinner or an empty container. The fix is telling the browser to wait for the right signal before the shutter fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  How wait strategies work
&lt;/h2&gt;

&lt;p&gt;Two parameters control the wait:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;waitUntil&lt;/code&gt;&lt;/strong&gt; — a network or lifecycle event: &lt;code&gt;"load"&lt;/code&gt;, &lt;code&gt;"domcontentloaded"&lt;/code&gt;, &lt;code&gt;"networkidle0"&lt;/code&gt;, or &lt;code&gt;"networkidle2"&lt;/code&gt; (default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;waitForSelector&lt;/code&gt;&lt;/strong&gt; — a CSS selector that appears when the content you care about is ready&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  React
&lt;/h2&gt;

&lt;p&gt;Add a marker element that renders after the app mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app-loaded"&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* rest of your app */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// capture.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RENDEX_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://myapp.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#app-loaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domcontentloaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Vue 3
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- App.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMounted&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;onMounted&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isReady"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"vue-ready"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://vueapp.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#vue-ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domcontentloaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Svelte
&lt;/h2&gt;

&lt;p&gt;For most Svelte apps, &lt;code&gt;networkidle0&lt;/code&gt; is sufficient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://svelteapp.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Selector never appears and capture times out.&lt;/strong&gt; Validate in DevTools with &lt;code&gt;document.querySelector('#your-selector')&lt;/code&gt;. If it returns &lt;code&gt;null&lt;/code&gt;, the element does not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial render even with selector.&lt;/strong&gt; The selector appeared but assets had not loaded. Add a &lt;code&gt;delay&lt;/code&gt; in milliseconds or wait for a more specific element.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;networkidle0&lt;/code&gt; times out.&lt;/strong&gt; The page has long-polling or WebSocket connections. Switch to &lt;code&gt;"networkidle2"&lt;/code&gt; or use &lt;code&gt;waitForSelector&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/blog/capture-screenshots-python" rel="noopener noreferrer"&gt;Python screenshot guide&lt;/a&gt; — same wait params with the Python SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; — full parameter list&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;Free screenshot tool&lt;/a&gt; — test selectors against a live URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; and start with 100 captures per month.&lt;/p&gt;

</description>
      <category>spa</category>
      <category>react</category>
      <category>vue</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Make Screenshot APIs Fast: 5 Optimizations</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 14 Aug 2026 16:50:12 +0000</pubDate>
      <link>https://dev.to/danelliot/how-to-make-screenshot-apis-fast-5-optimizations-26gd</link>
      <guid>https://dev.to/danelliot/how-to-make-screenshot-apis-fast-5-optimizations-26gd</guid>
      <description>&lt;h1&gt;
  
  
  How to Make Screenshot APIs Fast: 5 Optimizations
&lt;/h1&gt;

&lt;p&gt;Most screenshot API calls take longer than they need to. The bottleneck is rarely the API itself. It is the choices you make when calling it: the output format, the viewport size, which resources the browser loads, and whether you send one request or fifty. Five adjustments cover the majority of avoidable overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Use a compressed output format
&lt;/h2&gt;

&lt;p&gt;PNG is lossless and therefore the largest output format by default. For thumbnails, preview cards, and social images where exact pixel accuracy is not required, WebP or JPEG cut file size by 50 to 80 percent with no perceptible quality loss at typical display sizes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RENDEX_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// WebP — ~38 KB vs 210 KB for PNG&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;capture.webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Right-size the viewport
&lt;/h2&gt;

&lt;p&gt;The default viewport is 1280×800 at 2× device pixel ratio. For thumbnail generation, set &lt;code&gt;width&lt;/code&gt; to your target size instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thumb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deviceScaleFactor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Block resources you do not need
&lt;/h2&gt;

&lt;p&gt;Third-party analytics scripts, font files, and video embeds add load time. Use &lt;code&gt;blockResourceTypes&lt;/code&gt; to skip them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://dashboard.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;blockResourceTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;font&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;media&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Choose the right wait strategy
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;waitUntil: "load"&lt;/code&gt; fires faster than the default &lt;code&gt;"networkidle2"&lt;/code&gt; for server-rendered pages. Use &lt;code&gt;waitForSelector&lt;/code&gt; for client-side apps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fast server-rendered page&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://internal-dashboard.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// SPA with async content&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#main-content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domcontentloaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Submit multiple captures in one batch call
&lt;/h2&gt;

&lt;p&gt;The batch endpoint accepts 5 to 500 URLs per call and processes them concurrently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batchResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;productUrls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;blockResourceTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;font&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;media&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://yourapp.com/webhooks/screenshots&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Page looks broken after blocking resources.&lt;/strong&gt; Start with only &lt;code&gt;"font"&lt;/code&gt; and &lt;code&gt;"media"&lt;/code&gt;. Pages that use web fonts for icons will break when fonts are blocked — inject a fallback with &lt;code&gt;css: "* { font-family: sans-serif !important; }"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;waitUntil: "load"&lt;/code&gt; returns a blank page.&lt;/strong&gt; The page renders content after the load event. Use &lt;code&gt;waitForSelector&lt;/code&gt; with a CSS selector that appears when content is ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch results have some failed jobs.&lt;/strong&gt; Poll &lt;code&gt;/v1/jobs/:jobId&lt;/code&gt; for each job. Failed jobs return an error code and message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;Free screenshot tool&lt;/a&gt; — test format and viewport settings without code&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/blog/spa-screenshot-waitfor" rel="noopener noreferrer"&gt;SPA screenshot waitFor guide&lt;/a&gt; — framework-specific selector patterns&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; — full batch schema and plan-based limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; (100 calls/month, no credit card) or see &lt;a href="https://rendex.dev/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; for higher-volume plans.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Bulk Screenshot and PDF Generation with a Batch API</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 24 Jul 2026 17:29:30 +0000</pubDate>
      <link>https://dev.to/danelliot/bulk-screenshot-and-pdf-generation-with-a-batch-api-3bk2</link>
      <guid>https://dev.to/danelliot/bulk-screenshot-and-pdf-generation-with-a-batch-api-3bk2</guid>
      <description>&lt;h1&gt;
  
  
  Bulk Screenshot and PDF Generation with a Batch API
&lt;/h1&gt;

&lt;p&gt;Firing 200 separate screenshot requests one at a time works fine in a script. It falls apart in production: rate limits kick in, error handling gets messy, and you have no way to track overall progress. The batch screenshot API solves this with a single request: submit up to 500 URLs, get a batch ID back, then poll or receive a webhook when all jobs finish.&lt;/p&gt;

&lt;p&gt;This guide covers submitting a batch, polling for results, fanning out with a webhook, and knowing when batch beats firing N single requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Batch vs. Single Requests
&lt;/h2&gt;

&lt;p&gt;Single requests are fine for on-demand captures (a user clicks "screenshot", you capture, you display). Batch is the right choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a fixed list of URLs to capture in one job (site audit, competitor scan, monitoring baseline).&lt;/li&gt;
&lt;li&gt;You want a single webhook callback when all jobs are done rather than N separate callbacks.&lt;/li&gt;
&lt;li&gt;You need a shared progress indicator (34 of 48 complete) without building your own state machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Batch is URL-only. If you need to render raw HTML payloads, fire those as async single requests with &lt;code&gt;async: true&lt;/code&gt; using the &lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;REST API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan Limits
&lt;/h2&gt;

&lt;p&gt;Batch size caps are per request, not per day. You can fire multiple batches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Max URLs per batch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Credits are charged for the whole batch up front at submission time. If the batch fails before any jobs run, the charge is refunded automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Submit a Batch
&lt;/h2&gt;

&lt;p&gt;The batch screenshot API accepts a &lt;code&gt;urls&lt;/code&gt; array and a shared &lt;code&gt;defaults&lt;/code&gt; object for capture settings. Settings in &lt;code&gt;defaults&lt;/code&gt; apply to every URL in the batch. Source-type fields (&lt;code&gt;url&lt;/code&gt;, &lt;code&gt;html&lt;/code&gt;, &lt;code&gt;markdown&lt;/code&gt;) are not allowed in &lt;code&gt;defaults&lt;/code&gt; and return a 400 if present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot/batch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "urls": [
      "https://example.com",
      "https://example.com/pricing",
      "https://example.com/about"
    ],
    "defaults": {
      "format": "png",
      "fullPage": true,
      "width": 1280
    }
  }'&lt;/span&gt;

&lt;span class="c"&gt;# 202 Accepted — response includes batchId and per-job IDs:&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "batchId": "a3f9c2e1-...",&lt;/span&gt;
&lt;span class="c"&gt;#   "totalJobs": 3,&lt;/span&gt;
&lt;span class="c"&gt;#   "jobs": [&lt;/span&gt;
&lt;span class="c"&gt;#     { "jobId": "job_01jyav...", "url": "https://example.com", "status": "queued" },&lt;/span&gt;
&lt;span class="c"&gt;#     ...&lt;/span&gt;
&lt;span class="c"&gt;#   ]&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JS SDK wraps this in &lt;code&gt;rendex.batch()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// npm install @copperline/rendex&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rdx_YOUR_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// get one at rendex.dev/login&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/pricing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/about&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// "a3f9c2e1-..."&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalJobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python SDK uses &lt;code&gt;rendex.batch()&lt;/code&gt; with snake_case parameter names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;

&lt;span class="c1"&gt;# pip install rendex
&lt;/span&gt;&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rdx_YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# get one at rendex.dev/login
&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/about&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullPage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;batch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batchId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Submitted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;totalJobs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; jobs, batch &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Poll for Results
&lt;/h2&gt;

&lt;p&gt;The batch runs asynchronously on Cloudflare Queues. Poll &lt;code&gt;GET /v1/batches/:batchId&lt;/code&gt; to check progress. Each job in the response includes a &lt;code&gt;resultUrl&lt;/code&gt; (signed R2 link, valid for 24 hours by default) once its status is &lt;code&gt;completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9g1gly76bb2y99gywran.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9g1gly76bb2y99gywran.png" alt="Rendex batch screenshot API status panel showing 34 of 48 batch jobs completed, with per-URL status pills and a progress bar" width="800" height="1031"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rdx_YOUR_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batchStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completedJobs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalJobs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; done`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="s2"&gt;` (failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failedJobs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;partial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// signed PNG URL&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;waitForBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a3f9c2e1-...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The batch &lt;code&gt;status&lt;/code&gt; field is one of: &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt;, &lt;code&gt;partial&lt;/code&gt; (some jobs failed), or &lt;code&gt;failed&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Receive a Webhook When Done
&lt;/h2&gt;

&lt;p&gt;Polling works for scripts. For production services, pass &lt;code&gt;webhookUrl&lt;/code&gt; on the batch request. Rendex calls it once when all jobs in the batch have settled, with the same payload shape as the poll response. The request is HMAC-signed so you can verify the source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot/batch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "urls": ["https://example.com", "https://example.com/pricing"],
    "defaults": { "format": "png", "fullPage": true },
    "webhookUrl": "https://your-app.example.com/hooks/rendex-batch"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: Next.js App Router handler&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyWebhookSignature&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex/webhooks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rendex-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RENDEX_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bad signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultUrls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Batch &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; settled (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;): &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resultUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; screenshots ready`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Controlling How Long Result URLs Last
&lt;/h2&gt;

&lt;p&gt;By default, signed result URLs expire after 24 hours. Use the &lt;code&gt;cacheTtl&lt;/code&gt; field (in seconds) to extend or shorten this. The minimum is 3,600 seconds (1 hour) and the maximum is 2,592,000 seconds (30 days).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cacheTtl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;604800&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BATCH_LIMIT_EXCEEDED (400)&lt;/strong&gt;: Your batch exceeds the per-plan URL cap. Either split into smaller batches or upgrade. The error response includes the cap for your current plan and the next tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some jobs show status "failed"&lt;/strong&gt;: Individual jobs can fail without failing the whole batch. Check the &lt;code&gt;error&lt;/code&gt; field on each job entry. Common causes: the URL returned a non-200 status, the page timed out, or a wait selector was not found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhook not received&lt;/strong&gt;: Rendex validates the webhook URL against a block list (private IPs, loopback). The URL must be publicly reachable. For local development, use a tunnel like &lt;code&gt;cloudflared tunnel&lt;/code&gt; or ngrok and poll instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PLAN_UPGRADE_REQUIRED on a batch with geo&lt;/strong&gt;: The &lt;code&gt;geo&lt;/code&gt; parameter in &lt;code&gt;defaults&lt;/code&gt; requires a Pro or Enterprise plan. Free and Starter batches cannot use geo-targeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Batch capture works well alongside scheduled monitoring. The &lt;a href="https://rendex.dev/blog/website-monitoring-screenshots" rel="noopener noreferrer"&gt;website monitoring with automated screenshots&lt;/a&gt; guide shows a Cloudflare Workers cron that fires a batch of key pages hourly and alerts on visual changes.&lt;/p&gt;

&lt;p&gt;For the full request schema and all &lt;code&gt;defaults&lt;/code&gt; options, see the &lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ready to run your first batch? The &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;free screenshot tool&lt;/a&gt; lets you try a single capture without code. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get an API key&lt;/a&gt; (100 free calls/month, no credit card) to start submitting batches.&lt;/p&gt;

</description>
      <category>batch</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>HTML Table to PNG in Python Without wkhtmltoimage</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:21:26 +0000</pubDate>
      <link>https://dev.to/danelliot/html-table-to-png-in-python-without-wkhtmltoimage-13fi</link>
      <guid>https://dev.to/danelliot/html-table-to-png-in-python-without-wkhtmltoimage-13fi</guid>
      <description>&lt;p&gt;You have an HTML table (a financial summary, a styled DataFrame, a report section your frontend already renders) and you need it as a PNG from a Python backend. For a decade the reflex answer was &lt;code&gt;wkhtmltoimage&lt;/code&gt; (via imgkit) or a full Selenium driver. Both still technically run in 2026, and both are the wrong place to start a new project: the &lt;code&gt;wkhtmltoimage&lt;/code&gt; engine was archived in January 2023, and Selenium drags a whole browser fleet behind one PNG.&lt;/p&gt;

&lt;p&gt;This is the opinionated, decision-first version. You have a table, you want a PNG (and probably a PDF), and you do not want a frozen WebKit binary or a Chromedriver version-match problem in your deploy. There are exactly two paths worth your time: call a rendering API, or self-host Playwright. For the full six-way survey with every library benchmarked, see &lt;a href="https://rendex.dev/blog/render-html-table-to-png-python" rel="noopener noreferrer"&gt;Render an HTML Table to PNG in Python (5 Ways)&lt;/a&gt;. This post picks the winners and skips the legacy detours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1: a rendering API (nothing to host)
&lt;/h2&gt;

&lt;p&gt;POST the HTML, get PNG bytes back. A real, current Chromium runs on Cloudflare's edge through &lt;a href="https://rendex.dev/use-cases/server-side-report-rendering" rel="noopener noreferrer"&gt;Rendex&lt;/a&gt;, so modern CSS (Flexbox, Grid, web fonts) renders exactly as it does in Chrome, and there is no browser to install, patch, or keep alive in your image. The &lt;a href="https://pypi.org/project/rendex/" rel="noopener noreferrer"&gt;rendex Python SDK&lt;/a&gt; wraps the call in two lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install rendex   (key from rendex.dev/login)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;

&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RENDEX_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;table_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;table style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;border-collapse:collapse;font-family:system-ui;font-size:14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
  &amp;lt;thead&amp;gt;
    &amp;lt;tr style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background:#f1f5f9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
      &amp;lt;th style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:left;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Line item&amp;lt;/th&amp;gt;
      &amp;lt;th style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Amount&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
  &amp;lt;/thead&amp;gt;
  &amp;lt;tbody&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Subscriptions&amp;lt;/td&amp;gt;
        &amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Professional services&amp;lt;/td&amp;gt;
        &amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;€91,400&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_scale_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rendered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the table is the only thing on the page, render at &lt;code&gt;device_scale_factor=2&lt;/code&gt; for a crisp 2x PNG, and add &lt;code&gt;full_page=True&lt;/code&gt; when a long report runs past the viewport. The SDK posts to &lt;code&gt;/v1/screenshot&lt;/code&gt; under the hood and hands you the image bytes; if you would rather skip the dependency, the same render over raw HTTP returns those bytes straight to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RENDEX_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "html": "&amp;lt;table style=\"border-collapse:collapse;font-family:system-ui\"&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;",
    "format": "png",
    "deviceScaleFactor": 2,
    "fullPage": true
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole pipeline. It deploys to a serverless function or a slim container with no Chromium binary in the image, which is the part that usually breaks at 2am. The free tier is &lt;strong&gt;100 renders/month, no card&lt;/strong&gt;, so you can prove the report flow before you pay for anything. The browser-based test of the same request lives in the &lt;a href="https://rendex.dev/tools/html-to-image" rel="noopener noreferrer"&gt;HTML-to-image playground&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same call, now a PDF
&lt;/h2&gt;

&lt;p&gt;Most report pipelines need the table as a PNG for a dashboard &lt;em&gt;and&lt;/em&gt; as a PDF for an emailed deliverable. It is the same request with one field changed, so you do not bolt on a second library or a PDF-to-PNG conversion step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Same html, format="pdf" → PDF bytes back
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RENDEX_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendex renders to PNG, JPEG, WebP, or PDF from the same endpoint. For the REST field reference (every option, every output format), see the &lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;; the three-minute setup is in the &lt;a href="https://rendex.dev/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: self-host Playwright
&lt;/h2&gt;

&lt;p&gt;If the HTML cannot leave your network, or you already run a browser for other jobs, Playwright is the modern self-hosted answer. It ships evergreen bundled browsers, so there is no Chromedriver to version-match. &lt;code&gt;set_content()&lt;/code&gt; loads the HTML, then you screenshot the table element directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install playwright  &amp;amp;&amp;amp;  playwright install chromium
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="n"&gt;table_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_scale_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# crop to the table
&lt;/span&gt;    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works and you keep every byte local. The cost is ownership: the bundled browsers add hundreds of MB to your image, you patch them on Playwright's cadence, and a long-running process leaks memory and leaves zombie processes if you forget to close contexts. That is a fair trade when data residency is the constraint, and a poor one when rendering a table is a side-task to your actual report logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to skip, and why
&lt;/h2&gt;

&lt;p&gt;The legacy options are not broken so much as retired. Reach for them only to keep an existing script breathing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;wkhtmltoimage&lt;/code&gt; / imgkit&lt;/strong&gt;: the engine was archived in January 2023 (last stable release June 2020). It is a frozen old WebKit, so modern CSS Flex/Grid, web fonts, and emoji are broken or silently dropped, and a headless box throws &lt;code&gt;QXcbConnection: Could not connect to display&lt;/code&gt; unless you wrap every call in &lt;code&gt;xvfb-run&lt;/code&gt;. Fine for a legacy report; not a 2026 starting point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selenium&lt;/strong&gt;: maintained and battle-tested, but it is the most boilerplate for one PNG, full-page capture is not first-class, and you still own the browser fleet, same as Playwright with more ceremony.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WeasyPrint&lt;/strong&gt;: an excellent pure-Python engine, but it renders to PDF only (PNG export was removed in v53) and runs no JavaScript. Great for static HTML-to-PDF, useless when you need a PNG or your table is built by JS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest, side-by-side trade-offs for all five live in the &lt;a href="https://rendex.dev/blog/render-html-table-to-png-python" rel="noopener noreferrer"&gt;5-ways comparison&lt;/a&gt;, and the hosted contenders are scored in &lt;a href="https://rendex.dev/compare/best-html-to-image-apis-2026" rel="noopener noreferrer"&gt;best HTML-to-image APIs 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision in one line
&lt;/h2&gt;

&lt;p&gt;If you can send the HTML out, call the API: no browser to host, PNG or PDF from one request, and your deploy stays a plain Python service. If the HTML must stay in-network, self-host Playwright and accept that you now operate a browser. Either way, the answer to "HTML table to PNG in Python" in 2026 is not &lt;code&gt;wkhtmltoimage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fastest way to see the API path is to render your own table: &lt;a href="https://rendex.dev/tools/html-to-image" rel="noopener noreferrer"&gt;paste it into the playground&lt;/a&gt;, then &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;grab a free key&lt;/a&gt; and move the same call into your report job. 100 renders a month, no card, enough to ship the pipeline before you scale it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Async Screenshot API With Webhook Delivery</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:20:55 +0000</pubDate>
      <link>https://dev.to/danelliot/async-screenshot-api-with-webhook-delivery-1pgd</link>
      <guid>https://dev.to/danelliot/async-screenshot-api-with-webhook-delivery-1pgd</guid>
      <description>&lt;p&gt;A synchronous screenshot call holds the connection open until the page finishes rendering. That is fine for a fast landing page. It gets painful when you capture a heavy dashboard, a slow single-page app, or a full scrollable page: the request can run for tens of seconds, your worker sits blocked, and a proxy timeout can drop the response before the image is ready.&lt;/p&gt;

&lt;p&gt;The async screenshot API removes that wait. You submit the capture, get a job ID back immediately, and Rendex does the work in the background. When the job settles, it POSTs an HMAC-signed webhook to a URL you control. No open connection, no polling loop, no timeout risk. This guide covers the submit call, the signed payload, how to verify it in Node and Python, and how retries and idempotency work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmc7otlhxevyb5chil5r5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmc7otlhxevyb5chil5r5.png" alt="Async capture to signed webhook: a submit request with async and webhookUrl returns a job ID, and Rendex POSTs a signed job.completed webhook when the render is ready" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for async
&lt;/h2&gt;

&lt;p&gt;Reach for async and webhooks when a capture is slow or when you are firing a lot of them at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full-page captures of long or image-heavy pages.&lt;/li&gt;
&lt;li&gt;Single-page apps that need a few seconds to hydrate before they look right.&lt;/li&gt;
&lt;li&gt;Background jobs where nothing is waiting on the response in real time.&lt;/li&gt;
&lt;li&gt;High volume, so you never want a request holding a socket open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a handful of quick captures, the synchronous &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;screenshot endpoint&lt;/a&gt; is simpler. For rendering many URLs in one shot, see the &lt;a href="https://rendex.dev/blog/batch-rendering-api" rel="noopener noreferrer"&gt;batch rendering API&lt;/a&gt;, which also delivers a webhook when the whole batch is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Submit a non-blocking capture
&lt;/h2&gt;

&lt;p&gt;Send the same body you would to a normal capture, plus &lt;code&gt;async: true&lt;/code&gt; and a &lt;code&gt;webhookUrl&lt;/code&gt;. The response comes back right away with a &lt;code&gt;jobId&lt;/code&gt; and an HTTP 202, so your code moves on instead of waiting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RENDEX_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://news.ycombinator.com",
    "fullPage": true,
    "async": true,
    "webhookUrl": "https://api.your-app.com/hooks/rendex"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1f9c2e4-8b3d-4c7a-9f10-2b6e5c1d4a70"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"queued"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"req_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-20T18:24:01.004Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like every Rendex JSON endpoint, the response is wrapped in &lt;code&gt;{ success, data, meta }&lt;/code&gt;, so the job ID is at &lt;code&gt;data.jobId&lt;/code&gt; (the webhook payload below is the one exception: it is POSTed as a raw, unwrapped object). The submit charges one render credit up front. The webhook URL is checked before the job is queued, so a private or unreachable address is rejected at submit time rather than failing silently later. Each plan caps how many jobs can be in flight at once (queued or processing): 3 on Free, 50 on Starter, 200 on Pro. Go over and the submit returns a &lt;code&gt;QUEUE_LIMIT_REACHED&lt;/code&gt; response so you can back off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The signed callback
&lt;/h2&gt;

&lt;p&gt;When the capture finishes, Rendex sends a POST to your &lt;code&gt;webhookUrl&lt;/code&gt;. A completed job carries the event name, the job ID, and a signed URL to the rendered image. A failed job carries a safe error message instead of a result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"job.completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1f9c2e4-8b3d-4c7a-9f10-2b6e5c1d4a70"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resultUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.rendex.dev/v1/images/a1f9c2e4..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"completedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-20T18:24:07.512Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every delivery carries four headers you will use to verify and de-duplicate it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x-rendex-event&lt;/code&gt;: the event name, such as &lt;code&gt;job.completed&lt;/code&gt; or &lt;code&gt;job.failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-rendex-signature&lt;/code&gt;: the HMAC-SHA256 signature of the payload, hex encoded.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-rendex-timestamp&lt;/code&gt;: the Unix time in seconds when the signature was made.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-rendex-delivery-id&lt;/code&gt;: a stable ID that is the same across every retry of one delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Verify the signature
&lt;/h2&gt;

&lt;p&gt;Anyone who learns your webhook URL could POST to it, so treat an unsigned request as untrusted. The signature is an HMAC-SHA256 over the string &lt;code&gt;timestamp + "." + rawBody&lt;/code&gt;, keyed with your webhook secret. This is the same scheme Stripe uses, so the pattern will look familiar. Compute the expected signature over the raw request body and compare it in constant time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Express: use the RAW body, not the parsed JSON object.&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/hooks/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-rendex-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-rendex-timestamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEBHOOK_SECRET&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same check in Python with Flask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hooks/rendex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rendex_hook&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-rendex-signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-rendex-timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;received&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to get right. Verify against the exact bytes you received: if your framework parses JSON and re-serializes it, the whitespace changes and the signature will not match, so read the raw body. And use a constant-time compare (&lt;code&gt;timingSafeEqual&lt;/code&gt; or &lt;code&gt;hmac.compare_digest&lt;/code&gt;) instead of &lt;code&gt;===&lt;/code&gt; so you do not leak timing information. The full reference lives in the &lt;a href="https://rendex.dev/docs/webhooks" rel="noopener noreferrer"&gt;webhooks docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Polling as a fallback
&lt;/h2&gt;

&lt;p&gt;Webhooks are the right default, but you cannot always run a public endpoint, for example during local development or inside a locked-down network. In that case, skip the &lt;code&gt;webhookUrl&lt;/code&gt; and poll the job by ID instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.rendex.dev/v1/jobs/a1f9c2e4-8b3d-4c7a-9f10-2b6e5c1d4a70 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RENDEX_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# While running:  { "success": true, "data": { "status": "processing" } }&lt;/span&gt;
&lt;span class="c"&gt;# When done:      { "success": true, "data": { "status": "completed", "resultUrl": "https://api.rendex.dev/v1/images/..." } }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Poll on a gentle interval (a few seconds) and stop once &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;completed&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;. When you can receive a callback, prefer the webhook: it fires the moment the job settles instead of on your next poll, and it costs you no extra requests. Full field details are in the &lt;a href="https://rendex.dev/docs/api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries and idempotency
&lt;/h2&gt;

&lt;p&gt;If your endpoint does not answer with a 2xx, Rendex retries the delivery with exponential backoff, up to four attempts total, then gives up. A 4xx response is treated as a permanent rejection and is not retried, so a bug in your handler will not trigger a retry storm. Redirects are not followed: your webhook route should accept the POST directly.&lt;/p&gt;

&lt;p&gt;Because a slow acknowledgement can cause a retry after the work is already done, deliveries can arrive more than once. Use the &lt;code&gt;x-rendex-delivery-id&lt;/code&gt; header, which stays the same across retries of the same delivery, as an idempotency key: record the IDs you have processed and ignore repeats. Answer with a 2xx as soon as you have stored the event, then do slow work afterward, so a heavy handler never looks like a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No webhook arrives.&lt;/strong&gt; The URL must be public and resolve to a routable address. Private and internal hosts are blocked at submit time and return &lt;code&gt;INVALID_WEBHOOK_URL&lt;/code&gt;. During local work, use a tunnel that gives you a public URL, or poll instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature never matches.&lt;/strong&gt; You are almost certainly hashing a re-serialized body. Sign the raw bytes, and confirm the message is &lt;code&gt;timestamp + "." + body&lt;/code&gt; in that order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliveries stopped after one failure.&lt;/strong&gt; Your endpoint returned a 4xx, which is treated as permanent. Return a 5xx if you want the retry, or fix the handler and re-submit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;QUEUE_LIMIT_REACHED&lt;/code&gt; on submit.&lt;/strong&gt; You have hit your plan's concurrent-job cap. Wait for jobs to finish or move to a higher tier for more in-flight jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ship it
&lt;/h2&gt;

&lt;p&gt;Async plus webhooks turns screenshots into a background job your app never waits on: submit, get a job ID, and handle a signed callback when the image is ready. Grab an API key and run your first async capture from the &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;screenshot tool&lt;/a&gt;, or read the &lt;a href="https://rendex.dev/docs/webhooks" rel="noopener noreferrer"&gt;webhooks docs&lt;/a&gt; for the full payload and event list. New accounts include a free monthly render allowance, so you can wire up the whole flow before you pay anything. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Create an account&lt;/a&gt; to get your key.&lt;/p&gt;

</description>
      <category>async</category>
      <category>webhooks</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Add a Cursor MCP Screenshot Tool in Minutes</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Wed, 15 Jul 2026 17:15:38 +0000</pubDate>
      <link>https://dev.to/danelliot/add-a-cursor-mcp-screenshot-tool-in-minutes-3n37</link>
      <guid>https://dev.to/danelliot/add-a-cursor-mcp-screenshot-tool-in-minutes-3n37</guid>
      <description>&lt;p&gt;Cursor's AI can read every file in your project, but it cannot see a live web page. Ask it about a layout bug or an OG image and it is working blind. The Model Context Protocol (MCP) fixes that: wire in a screenshot server and Cursor gains a tool it can call on its own to capture any URL and reason about the result.&lt;/p&gt;

&lt;p&gt;This guide sets up a Cursor MCP screenshot tool with Rendex in about five minutes. You get one tool, &lt;code&gt;rendex_screenshot&lt;/code&gt;, that turns a URL, raw HTML, or Markdown into a PNG, JPEG, WebP, or PDF, with options like full-page capture and dark mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytf6mi61864nmluul1mn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytf6mi61864nmluul1mn.png" alt="Cursor MCP screenshot config on the left and the captured rendex.dev pricing page on the right" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need three things before you start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursor installed (any recent version with MCP support).&lt;/li&gt;
&lt;li&gt;Node.js on your machine, so Cursor can run the server with &lt;code&gt;npx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A Rendex API key. The &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;free tier&lt;/a&gt; gives you 100 renders a month with no credit card, which is plenty for editor use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Get a Rendex API key
&lt;/h2&gt;

&lt;p&gt;Sign in at &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;rendex.dev/login&lt;/a&gt;, open the dashboard, and copy your key. It looks like &lt;code&gt;rdx_&lt;/code&gt; followed by a short string. Keep it out of version control. If you want the exact key format and rate limits, the &lt;a href="https://rendex.dev/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add Rendex to .cursor/mcp.json
&lt;/h2&gt;

&lt;p&gt;Cursor reads MCP servers from &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in your project root, or from Settings then MCP for a global setup. Create the file and add the Rendex server. The &lt;code&gt;npx&lt;/code&gt; command downloads and runs the published package, so there is nothing to install by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rendex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@copperline/rendex-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"RENDEX_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rdx_your_key_here"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;.cursor/mcp.json&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt;. It holds your key, so it should never land in a commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Or connect the hosted server (no install)
&lt;/h2&gt;

&lt;p&gt;If you would rather skip the local process, point Cursor at the hosted server instead. It is fronted by OAuth 2.1, so you sign in with a one-time email code and never paste a key into a config file. Each caller bills against their own plan and credit pool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rendex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.rendex.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clients that prefer a static key can still pass one in an &lt;code&gt;Authorization&lt;/code&gt; header. The &lt;a href="https://rendex.dev/docs/mcp" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt; show both the OAuth flow and the header form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Restart Cursor and confirm the tool
&lt;/h2&gt;

&lt;p&gt;Fully quit and reopen Cursor so it reloads the config. Open the MCP settings and check that the &lt;code&gt;rendex&lt;/code&gt; server shows a green status with &lt;code&gt;rendex_screenshot&lt;/code&gt; listed under it. If the server is red, the JSON is almost always the cause. See troubleshooting below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Ask Cursor to capture a page
&lt;/h2&gt;

&lt;p&gt;Now you can prompt Cursor in plain language and it will call the tool on its own. Try something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Screenshot https://rendex.dev/pricing in dark mode,
full page, and tell me if the plan prices are readable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor calls &lt;code&gt;rendex_screenshot&lt;/code&gt; with the arguments it parsed from your request, gets the image back, and reasons about it. The tool accepts the same options the API does, so you can be specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://rendex.dev/pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fullPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"darkMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iphone_15"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful parameters to know: &lt;code&gt;format&lt;/code&gt; switches between &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, &lt;code&gt;webp&lt;/code&gt;, and &lt;code&gt;pdf&lt;/code&gt;; &lt;code&gt;fullPage&lt;/code&gt; captures the whole scroll height; &lt;code&gt;selector&lt;/code&gt; crops to a single element like a pricing card; and &lt;code&gt;blockAds&lt;/code&gt; removes ad and cookie clutter before the shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The server shows red or the tool never appears.&lt;/strong&gt; Validate the JSON. A trailing comma or a missing brace stops Cursor from parsing the file. Paste it into a JSON linter, fix the error, then restart Cursor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every call returns an auth error.&lt;/strong&gt; The key is wrong or missing. Confirm &lt;code&gt;RENDEX_API_KEY&lt;/code&gt; is set in the &lt;code&gt;env&lt;/code&gt; block and that you copied the full &lt;code&gt;rdx_&lt;/code&gt; key from the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npx cannot find the package.&lt;/strong&gt; Check that Node.js is on your PATH and run &lt;code&gt;npx -y @copperline/rendex-mcp&lt;/code&gt; once in a terminal to prime the download, then reopen Cursor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want the same setup in another editor.&lt;/strong&gt; The config is nearly identical for Claude Desktop, Claude Code, and Windsurf. The &lt;a href="https://rendex.dev/blog/mcp-rendering-guide" rel="noopener noreferrer"&gt;MCP rendering guide&lt;/a&gt; walks through each client and the full tool set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;That is the whole setup. Cursor can now capture live pages while it works, which is handy for reviewing a deploy, checking an OG image, or grabbing a reference shot without leaving the editor. To try the same engine outside Cursor first, the browser-based &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;screenshot tool&lt;/a&gt; runs a capture with no code.&lt;/p&gt;

&lt;p&gt;When you are ready to wire it into Cursor, &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;grab a free API key&lt;/a&gt; and drop it into your config. One hundred renders a month is enough to keep the tool on hand all day.&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Render HTML and Screenshots in Zapier (No-Code)</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Fri, 26 Jun 2026 17:46:18 +0000</pubDate>
      <link>https://dev.to/danelliot/render-html-and-screenshots-in-zapier-no-code-1cin</link>
      <guid>https://dev.to/danelliot/render-html-and-screenshots-in-zapier-no-code-1cin</guid>
      <description>&lt;h1&gt;
  
  
  Render HTML and Screenshots in Zapier (No-Code)
&lt;/h1&gt;

&lt;p&gt;Zapier connects your apps, but getting a rendered image or PDF out of a Zap usually means wiring up an HTTP module and manually constructing JSON bodies. The native Rendex app for Zapier removes that step: five actions in the app directory, no HTTP modules, no auth headers to copy by hand.&lt;/p&gt;

&lt;p&gt;This guide covers the three most common workflows: capturing a URL as a PNG, rendering an HTML template with dynamic data using Template Values, and generating a PDF. All three work from the Zapier editor with point-and-click field mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Zapier account on any plan. The free plan supports single-step and two-step Zaps; Starter or higher unlocks multi-step workflows.&lt;/li&gt;
&lt;li&gt;A Rendex API key. The free tier covers 500 captures per month. &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get one at rendex.dev/login&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Find Rendex and Connect Your Account
&lt;/h2&gt;

&lt;p&gt;Open Zapier and start a new Zap. When adding an action step, search for &lt;strong&gt;Rendex&lt;/strong&gt; in the app directory. Zapier will prompt you to authenticate: paste your API key and click &lt;strong&gt;Yes, Continue&lt;/strong&gt;. Zapier validates the key against &lt;code&gt;api.rendex.dev&lt;/code&gt; and stores it encrypted. It never appears in Zap run logs.&lt;/p&gt;

&lt;p&gt;Full setup steps are in the &lt;a href="https://rendex.dev/docs/zapier" rel="noopener noreferrer"&gt;Zapier integration docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Rendex app exposes five actions and one trigger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capture Screenshot&lt;/strong&gt; — URL, HTML, or Markdown to PNG/JPEG/WebP/PDF (synchronous, returns the file inline)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate PDF&lt;/strong&gt; — same sources, always PDF output, with page-size, margin, and orientation controls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract Text&lt;/strong&gt; — pull clean content from a URL for LLM pipelines or content archives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture Screenshot (Background)&lt;/strong&gt; — async, returns a Job ID; pair with Get Job Status for heavy pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submit Batch&lt;/strong&gt; — 5 to 500 URLs at once depending on your Rendex plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New Completed Screenshot&lt;/strong&gt; (trigger) — fires when a background job finishes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Capture a Zapier Screenshot from a URL
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Capture Screenshot&lt;/strong&gt; is the right action for most Zaps. It runs synchronously: the step submits the render, waits for the result, and returns the image file inline. The next Zap step can attach it to an email, upload it to Google Drive, or post it to Slack.&lt;/p&gt;

&lt;p&gt;Set &lt;strong&gt;Source Type&lt;/strong&gt; to &lt;strong&gt;URL&lt;/strong&gt;, then map the URL field from your trigger: an Airtable URL column, a Google Sheets cell, or a field from an incoming webhook. Set &lt;strong&gt;Output Format&lt;/strong&gt; to &lt;code&gt;png&lt;/code&gt; and toggle &lt;strong&gt;Full Page&lt;/strong&gt; if you need the entire scrollable page, not just the above-the-fold viewport.&lt;/p&gt;

&lt;p&gt;The action returns a &lt;code&gt;file&lt;/code&gt; output. Map it into any binary-capable Zap step: Google Drive &lt;strong&gt;Upload File&lt;/strong&gt;, Dropbox, OneDrive, or an email attachment.&lt;/p&gt;

&lt;p&gt;A common Zapier screenshot recipe: &lt;strong&gt;Airtable New Record&lt;/strong&gt; (with a URL field) to &lt;strong&gt;Rendex Capture Screenshot&lt;/strong&gt; to &lt;strong&gt;Airtable Update Record&lt;/strong&gt; to write the screenshot URL back to the same row.&lt;/p&gt;

&lt;p&gt;For pages that load content client-side via JavaScript, set &lt;strong&gt;Wait For Selector&lt;/strong&gt; to a CSS selector that appears only after the page finishes rendering. Without this, the capture may fire before the content paints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Render an HTML Template with Dynamic Data
&lt;/h2&gt;

&lt;p&gt;When &lt;strong&gt;Source Type&lt;/strong&gt; is set to &lt;strong&gt;HTML&lt;/strong&gt;, a second field appears: &lt;strong&gt;Template Values&lt;/strong&gt;. This is the feature that separates Rendex from a plain screenshot API.&lt;/p&gt;

&lt;p&gt;Template Values is a key-value dict. Any key you add replaces the matching &lt;code&gt;{{placeholder}}&lt;/code&gt; in your HTML at render time. Write one HTML template with placeholders, fill in the values from your Zap trigger, and every run produces a different rendered output from the same template. No code step, no external template engine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvggt5e6rrjaazwcu9ytu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvggt5e6rrjaazwcu9ytu.png" alt="Branded invoice PDF rendered by Rendex from a Zapier Template Values dict with client name, invoice number, and total" width="800" height="1031"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The invoice above uses a single HTML template with &lt;code&gt;{{client}}&lt;/code&gt;, &lt;code&gt;{{invoice_no}}&lt;/code&gt;, and &lt;code&gt;{{total}}&lt;/code&gt; placeholders. Each Zap run fills them from a Google Sheets row. Markdown is supported the same way: set Source Type to &lt;strong&gt;Markdown&lt;/strong&gt; and Rendex styles the output automatically.&lt;/p&gt;

&lt;p&gt;To verify the template before building the Zap, use the free &lt;a href="https://rendex.dev/tools/html-to-image" rel="noopener noreferrer"&gt;HTML-to-image tool&lt;/a&gt;. You can also test the equivalent API call directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_live_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "html": "&amp;lt;div style=\"font-family:system-ui;padding:48px\"&amp;gt;&amp;lt;h1 style=\"color:#ea580c\"&amp;gt;Invoice #{{invoice_no}}&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Billed to: {{client}}&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Total due: {{total}}&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;",
    "data": {
      "invoice_no": "INV-2026-0431",
      "client": "Northwick Ventures",
      "total": "$3,045.00"
    },
    "format": "png",
    "width": 900,
    "height": 400
  }'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; invoice.png
&lt;span class="c"&gt;# Get your API key at https://rendex.dev/login&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;data&lt;/code&gt; field in the API body maps directly to Template Values in the Zapier editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Generate a PDF
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;Generate PDF&lt;/strong&gt; when you need a paginated document rather than an image. It accepts the same source types (URL, HTML, Markdown) and the same Template Values dict. PDF-specific fields appear in the action editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDF Page Size&lt;/strong&gt;: A4 (default), Letter, Legal, Tabloid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF Landscape&lt;/strong&gt;: toggle for wide content like dashboards or spreadsheets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF Print Background&lt;/strong&gt;: on by default, preserves CSS background colors and images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF Margins&lt;/strong&gt;: JSON object with top, right, bottom, left in CSS units&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common recipe: &lt;strong&gt;Stripe New Invoice&lt;/strong&gt; to &lt;strong&gt;Rendex Generate PDF&lt;/strong&gt; (map the hosted invoice URL) to &lt;strong&gt;Google Drive Upload File&lt;/strong&gt; to &lt;strong&gt;Gmail Send Email&lt;/strong&gt; with the Drive link.&lt;/p&gt;

&lt;p&gt;For slow-loading pages, switch to &lt;strong&gt;Capture Screenshot (Background)&lt;/strong&gt; to avoid Zapier's 30-second step timeout. The action returns a Job ID; add a &lt;strong&gt;Get Job Status&lt;/strong&gt; search step to retrieve the finished PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;API key not validating:&lt;/strong&gt; Confirm the key starts with &lt;code&gt;rdx_&lt;/code&gt;. Paste it directly from your Rendex dashboard to avoid trailing spaces. If you rotated the key, go to Zapier's &lt;strong&gt;Connected Accounts&lt;/strong&gt; and reconnect Rendex with the new key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot is blank or shows a loading spinner:&lt;/strong&gt; The page renders content after initial HTML load via JavaScript. Set &lt;strong&gt;Wait For Selector&lt;/strong&gt; to a CSS selector that only exists after the page finishes hydrating, for example &lt;code&gt;#main-content&lt;/code&gt; or &lt;code&gt;.loaded&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zap timed out:&lt;/strong&gt; Zapier caps synchronous action steps at 30 seconds. Switch to &lt;strong&gt;Capture Screenshot (Background)&lt;/strong&gt; for pages that take longer. Pair it with a &lt;strong&gt;Get Job Status&lt;/strong&gt; step to retrieve the result, or paste a Zapier Catch Hook URL into the Webhook URL field to trigger a follow-up Zap when the render finishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;403 PLAN_UPGRADE_REQUIRED:&lt;/strong&gt; Some fields (Cookies, Custom Headers, Geo Country) require a paid Rendex plan. Each field's tooltip notes the plan requirement. Upgrade at the &lt;a href="https://rendex.dev/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rendex.dev/docs/zapier" rel="noopener noreferrer"&gt;Zapier integration docs&lt;/a&gt; list every action, trigger, search, and field with plan requirements in a single reference table.&lt;/p&gt;

&lt;p&gt;For the equivalent workflow in Make.com, the same HTML-to-image pattern via Make's HTTP module is covered in the &lt;a href="https://rendex.dev/blog/make-html-to-image" rel="noopener noreferrer"&gt;Make HTML-to-image guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ready to start? &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; (500 renders/month, no credit card) and search for &lt;strong&gt;Rendex&lt;/strong&gt; in the Zapier app directory. The free &lt;a href="https://rendex.dev/tools/html-to-image" rel="noopener noreferrer"&gt;HTML-to-image tool&lt;/a&gt; is useful for testing your template before you build the Zap.&lt;/p&gt;

</description>
      <category>zapier</category>
      <category>nocode</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Rendex Watch: Visual Website-Change Monitoring</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Sun, 21 Jun 2026 17:26:22 +0000</pubDate>
      <link>https://dev.to/danelliot/rendex-watch-visual-website-change-monitoring-22p0</link>
      <guid>https://dev.to/danelliot/rendex-watch-visual-website-change-monitoring-22p0</guid>
      <description>&lt;h1&gt;
  
  
  Rendex Watch: Visual Website-Change Monitoring, Built Into Your Rendering API
&lt;/h1&gt;

&lt;p&gt;Rendex Watch is live. It monitors any URL on a schedule and shows you a real-Chrome before and after visual diff with a highlighted overlay, so you see exactly which part of the page changed, not just that something did. It runs inside the same platform as the rest of Rendex: one &lt;code&gt;rdx_&lt;/code&gt; key, one shared credit pool, one bill, alongside screenshots, PDF, and content extraction.&lt;/p&gt;

&lt;p&gt;Most uptime checks tell you the server returned a 200. They stay green while a deploy blanks a section, a price flips, a cookie banner covers the page, or a third-party script wipes the layout. Watch captures the rendered page on a schedule, compares each check against a stored baseline, and alerts you by email or webhook when the page actually changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;Point a watch at a URL and pick how it should detect change: a visual diff (a real Chrome render, compared pixel by pixel), a text diff (the extracted page text), or both. When a check crosses your threshold, Rendex stores the before frame, the after frame, and an overlay that rings the changed regions in orange, then fires your alerts. The change notification carries the before, after, and overlay images, so the alert itself shows what moved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxmqfkxdhwmeppnlh515.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxmqfkxdhwmeppnlh515.png" alt="Rendex Watch change alert showing a before and after of a status page with the changed regions ringed in orange and the change magnitude" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full surface is available the day it ships: a REST API under &lt;code&gt;/v1/watches&lt;/code&gt;, the JavaScript and Python SDKs, the Rendex MCP server for AI agents, a native n8n node, and a Zapier app. See the &lt;a href="https://rendex.dev/watch" rel="noopener noreferrer"&gt;Rendex Watch overview&lt;/a&gt; for the product tour, or the &lt;a href="https://rendex.dev/docs/watch" rel="noopener noreferrer"&gt;Watch API reference&lt;/a&gt; for every parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a visual diff catches more
&lt;/h2&gt;

&lt;p&gt;Text and DOM diffs miss anything that breaks visually without changing the markup: a CSS regression, an image that fails to load, a font swap, a layout shift from an ad slot, a render that white-screens on a JavaScript error. A real-browser visual diff catches those because it compares what the page actually drew. The overlay then tells you where to look instead of leaving you to hunt through a full-page screenshot. For monitors built around content rather than layout, the text diff runs in the same check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a watch
&lt;/h2&gt;

&lt;p&gt;A watch is one &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/v1/watches&lt;/code&gt;. Pass the URL, how it should detect change (&lt;code&gt;diffMode&lt;/code&gt; is &lt;code&gt;visual&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, or &lt;code&gt;both&lt;/code&gt;), and how often to check (&lt;code&gt;intervalMinutes&lt;/code&gt;). The optional &lt;code&gt;threshold&lt;/code&gt; is a 0 to 1 noise floor the visual diff must exceed before a check counts as changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get your API key at rendex.dev/login&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/watches &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer rdx_YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://example.com/pricing",
    "name": "Competitor pricing",
    "diffMode": "both",
    "intervalMinutes": 180,
    "threshold": 0.02,
    "webhookUrl": "https://your.app/hooks/rendex"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first check establishes the baseline. Every check after that compares against it, and the baseline advances when a real change is confirmed, so you are always measuring against the most recent known-good frame. Each scheduled check is one render credit from the same pool your screenshots and PDFs draw on.&lt;/p&gt;

&lt;h2&gt;
  
  
  From an SDK, MCP, or no-code
&lt;/h2&gt;

&lt;p&gt;The JavaScript and Python SDKs wrap the same endpoint. The JS SDK exposes &lt;code&gt;createWatch&lt;/code&gt;, &lt;code&gt;listWatches&lt;/code&gt;, &lt;code&gt;getWatch&lt;/code&gt;, &lt;code&gt;updateWatch&lt;/code&gt;, &lt;code&gt;deleteWatch&lt;/code&gt;, &lt;code&gt;runWatch&lt;/code&gt;, &lt;code&gt;listWatchRuns&lt;/code&gt;, and &lt;code&gt;testWatch&lt;/code&gt;; the Python SDK ships the snake_case equivalents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm install @copperline/rendex&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Rendex&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@copperline/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rdx_YOUR_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWatch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/pricing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;diffMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intervalMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your.app/hooks/rendex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For no-code and agent workflows, the surface is native. The Rendex MCP server adds &lt;code&gt;watch_create&lt;/code&gt;, &lt;code&gt;watch_list&lt;/code&gt;, &lt;code&gt;watch_get&lt;/code&gt;, &lt;code&gt;watch_run&lt;/code&gt;, &lt;code&gt;watch_test&lt;/code&gt;, &lt;code&gt;watch_runs&lt;/code&gt;, and &lt;code&gt;watch_delete&lt;/code&gt; so an AI agent can set up and read monitors. The n8n node adds a Watch resource and a "Rendex Watch Trigger" that starts a workflow when a page changes, and the Zapier app adds the same actions plus a "Website Changed" trigger. Want to see what Rendex captures for a URL before you commit a watch? The &lt;a href="https://rendex.dev/tools/preview-website-watch" rel="noopener noreferrer"&gt;Preview a Website Watch&lt;/a&gt; tool runs a one-time capture in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerts: email and webhook
&lt;/h2&gt;

&lt;p&gt;Email alerts work on every plan. Signed webhooks are available from the Starter tier and up. On a change, Rendex POSTs an HMAC-signed &lt;code&gt;watch.changed&lt;/code&gt; event (with &lt;code&gt;watch.recovered&lt;/code&gt; and &lt;code&gt;watch.error&lt;/code&gt; for the recovery and failure transitions). The payload carries the change magnitude and signed image URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"watch.changed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"watchId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8f3c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/pricing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"diffScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.067&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"diffPixels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"beforeUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://rendex.dev/i/...signed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"afterUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://rendex.dev/i/...signed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"diffOverlayUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://rendex.dev/i/...signed"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the signature before you trust the body. The JS and Python SDKs ship a &lt;code&gt;verifyWebhook&lt;/code&gt; helper that checks the HMAC against your signing secret. The same signing scheme backs the async render webhooks, documented in the &lt;a href="https://rendex.dev/docs/webhooks" rel="noopener noreferrer"&gt;webhooks reference&lt;/a&gt;. To clear or replace an alert channel later, send an &lt;code&gt;updateWatch&lt;/code&gt; call. The &lt;a href="https://rendex.dev/docs/sdks" rel="noopener noreferrer"&gt;SDK reference&lt;/a&gt; lists every method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and notes
&lt;/h2&gt;

&lt;p&gt;Watch counts and check frequency scale with your plan: Free monitors 1 page on a daily check, Starter 10 pages as often as every 3 hours, Pro 50 pages as often as every 30 minutes, and Enterprise 1,000 pages as often as every 5 minutes (counts are total, active plus paused). Geo-targeted checks, which render the page from a chosen country, are on Pro and Enterprise. There is no overage: if your credit pool runs out, the watch pauses rather than billing past the cap, and you resume it when you top up.&lt;/p&gt;

&lt;p&gt;A note on what is not here yet: the AI "what changed" written summary is on the roadmap, not shipped. Today the change event gives you the visual overlay, the change magnitude, and the text diff, which is enough to route and triage. For a feature-by-feature look at how Watch compares to standalone monitors, see &lt;a href="https://rendex.dev/compare/best-website-monitoring-tools-2026" rel="noopener noreferrer"&gt;the best website monitoring tools roundup&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;If you already use Rendex for screenshots or PDF, Watch needs no new account, key, or bill: it draws from the same credit pool. Create your first watch from the &lt;a href="https://rendex.dev/watch" rel="noopener noreferrer"&gt;Rendex Watch page&lt;/a&gt; or jump straight into the dashboard.&lt;/p&gt;

&lt;p&gt;Start for free at &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;rendex.dev/login&lt;/a&gt;. The free tier includes 1 watch on a daily check, with email alerts, alongside 500 render calls a month. No credit card required.&lt;/p&gt;

</description>
      <category>watch</category>
      <category>monitoring</category>
      <category>updatedriven</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Rendex Is Now a Verified n8n Community Node</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Mon, 15 Jun 2026 19:46:43 +0000</pubDate>
      <link>https://dev.to/danelliot/rendex-is-now-a-verified-n8n-community-node-1243</link>
      <guid>https://dev.to/danelliot/rendex-is-now-a-verified-n8n-community-node-1243</guid>
      <description>&lt;h1&gt;
  
  
  Rendex Is Now a Verified n8n Community Node
&lt;/h1&gt;

&lt;p&gt;The Rendex n8n node passed the n8n team's verification review and went live on n8n Cloud on April 30. Search "Rendex" on the n8n canvas and the node appears. No JSON file editing, no manual npm installs inside a self-hosted instance.&lt;/p&gt;

&lt;p&gt;This post covers what the verified Rendex n8n node provides, how to set it up in under five minutes, and how to use it as an AI Agent tool for autonomous web capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Verification Matters
&lt;/h2&gt;

&lt;p&gt;Before the verified status, adding Rendex to an n8n workflow meant using the generic HTTP module: configure the endpoint URL manually, build the request body by hand, set the Authorization header, and parse a binary response yourself. Every update to the API meant updating the HTTP node config.&lt;/p&gt;

&lt;p&gt;The verified Rendex n8n node replaces all of that with a purpose-built node: typed dropdowns for resources and operations, a dedicated credential type for your API key, and typed fields for every capture parameter. n8n's credential store handles the key; you never paste it into a request body.&lt;/p&gt;

&lt;p&gt;Verified status also means n8n reviews the node on each release before it reaches users. Self-hosted instances with verified community nodes enabled pull updates through the standard n8n update path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the Node
&lt;/h2&gt;

&lt;p&gt;On &lt;strong&gt;n8n Cloud&lt;/strong&gt;, the node is available by default. Open any workflow, click the "+" button to add a node, and search "Rendex".&lt;/p&gt;

&lt;p&gt;On a &lt;strong&gt;self-hosted instance&lt;/strong&gt;, go to &lt;strong&gt;Admin Panel &amp;gt; Community Nodes&lt;/strong&gt; and enable verified community nodes if the toggle is off. Then search the canvas as above. The package name is &lt;code&gt;n8n-nodes-rendex&lt;/code&gt; if you need it for allowlists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Credentials
&lt;/h2&gt;

&lt;p&gt;After adding the node to a workflow, click &lt;strong&gt;Credentials &amp;gt; Create New &amp;gt; Rendex API&lt;/strong&gt;. Paste your API key (get one at &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;rendex.dev/login&lt;/a&gt;). The key starts with &lt;code&gt;rdx_live_&lt;/code&gt;. Save the credential and the node is ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture a Screenshot
&lt;/h2&gt;

&lt;p&gt;Set &lt;strong&gt;Resource&lt;/strong&gt; to &lt;em&gt;Screenshot&lt;/em&gt; and &lt;strong&gt;Operation&lt;/strong&gt; to &lt;em&gt;Capture&lt;/em&gt;. Enter a URL, choose a format in Additional Options, and execute the workflow. The node outputs binary data (the image) alongside a JSON metadata item.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"screenshot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"capture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fullPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"viewportWidth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1440&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"blockAds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For PDF output, set &lt;code&gt;format&lt;/code&gt; to &lt;code&gt;pdf&lt;/code&gt; in Additional Options. Page size, margins, and print backgrounds are all configurable. See the &lt;a href="https://rendex.dev/docs/n8n" rel="noopener noreferrer"&gt;n8n integration docs&lt;/a&gt; for the full parameter list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Submit a Batch
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Batch &amp;gt; Submit&lt;/strong&gt; operation takes a newline-separated list of URLs and submits them as a single batch request. Rendex processes them in parallel and returns a batch ID. Poll the batch status with &lt;strong&gt;Batch &amp;gt; Get Status&lt;/strong&gt; or set a webhook URL in Additional Options to receive a callback when the batch finishes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"batch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://northwindanalytics.com/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;https://northwindanalytics.com/pricing&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;https://northwindanalytics.com/docs&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;https://northwindanalytics.com/blog&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;https://northwindanalytics.com/annual-report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fullPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batch limits depend on your plan. Each captured URL returns a signed storage URL you can pass to a downstream node to upload, email, or store. Try &lt;a href="https://rendex.dev/tools/screenshot" rel="noopener noreferrer"&gt;the free screenshot tool&lt;/a&gt; to test individual captures before wiring up a batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output: a Batch Capture Report
&lt;/h2&gt;

&lt;p&gt;The workflow below submits five URLs, waits for the batch to complete, and passes the results to a Code node that renders a report via the Rendex n8n node's &lt;em&gt;Screenshot &amp;gt; Capture&lt;/em&gt; operation with an HTML template.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz7wkukzy34xkq3t4inm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmz7wkukzy34xkq3t4inm.png" alt="Batch capture report generated by the Rendex n8n node showing 5 URLs captured as PNGs and PDFs with file sizes and timestamps" width="800" height="907"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Use as an AI Agent Tool
&lt;/h2&gt;

&lt;p&gt;The Rendex n8n node is built with &lt;code&gt;usableAsTool: true&lt;/code&gt;, which means you can attach it directly to an n8n AI Agent node. The agent receives the node as a callable tool and decides when to invoke it based on the conversation or task context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"n8n-nodes-base.agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"toolsAgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rendex"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice this means an AI Agent can capture a screenshot mid-task without any extra branching logic. The agent calls the Rendex n8n node the same way it calls a search tool or a calculator. Pair it with a vision model node downstream to analyze what was captured.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Node Covers
&lt;/h2&gt;

&lt;p&gt;The Rendex n8n node exposes four resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screenshot&lt;/strong&gt;: Capture (sync) and Capture Async. Sync returns binary data in the same workflow step. Async fires the job and returns a job ID immediately, which is useful for long-running full-page captures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document&lt;/strong&gt;: Extract. Turns any URL into clean Markdown, JSON, or article HTML pulled from the fully-rendered page. Useful for feeding scraped content to an LLM node downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job&lt;/strong&gt;: Get Status. Polls an async capture job for progress and the signed result URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch&lt;/strong&gt;: Submit and Get Status. Submit up to 500 URLs per request and track the batch through to completion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits and Notes
&lt;/h2&gt;

&lt;p&gt;A few things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Async polling&lt;/strong&gt;: Capture Async returns a job ID, not the image. Add a Job &amp;gt; Get Status node in a loop or configure a webhook URL in Additional Options so Rendex calls your n8n webhook trigger on completion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch inputs are URL-only&lt;/strong&gt;: The Batch Submit operation does not accept HTML or Markdown payloads in the URLs list. To render an HTML template to multiple outputs, loop over Screenshot &amp;gt; Capture with Source set to HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch size caps&lt;/strong&gt;: The maximum URLs per batch depends on your plan. Check &lt;a href="https://rendex.dev/pricing" rel="noopener noreferrer"&gt;the pricing page&lt;/a&gt; for plan-level limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary output&lt;/strong&gt;: Sync captures return binary data in n8n's binary item format. Connect to a Write Binary File node or an email attachment node downstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rendex.dev/docs/n8n" rel="noopener noreferrer"&gt;n8n integration docs&lt;/a&gt; list every parameter available in Additional Options, including wait strategies, viewport sizing, cookie injection, and geo-targeting.&lt;/p&gt;

&lt;p&gt;For a complete workflow that generates invoice PDFs from an HTML template triggered by a webhook, read &lt;a href="https://rendex.dev/blog/n8n-invoice-automation" rel="noopener noreferrer"&gt;Automate Invoice and Document Generation in n8n with Rendex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Get an API key at &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;rendex.dev/login&lt;/a&gt;. The free tier includes 500 captures per month to get started.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>updatedriven</category>
    </item>
    <item>
      <title>Render an HTML Table to PNG in Python (5 Ways, 2026)</title>
      <dc:creator>Dan E</dc:creator>
      <pubDate>Sun, 07 Jun 2026 17:15:07 +0000</pubDate>
      <link>https://dev.to/danelliot/render-an-html-table-to-png-in-python-5-ways-2026-1h5n</link>
      <guid>https://dev.to/danelliot/render-an-html-table-to-png-in-python-5-ways-2026-1h5n</guid>
      <description>&lt;p&gt;You have an HTML table — a financial summary, a report section, a styled DataFrame, a chart your frontend already renders — and you need it as a PNG from a Python backend. A cron job that emails a nightly report, an invoice rasterized into a PDF, a certificate generated per customer. The HTML is the easy part; turning it into a pixel-accurate image is where the options get messy.&lt;/p&gt;

&lt;p&gt;This guide covers five working approaches, from a single API call to a headless browser you host yourself, plus an honest note on where each one bites. Every snippet renders the same little table so you can compare them directly. (All facts verified June 2026 — wkhtmltopdf is now archived, which changes the usual advice.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Rendex API (no browser to host)
&lt;/h2&gt;

&lt;p&gt;The fastest path, and the one that needs nothing on your machine. POST the HTML to the &lt;a href="https://rendex.dev/use-cases/server-side-report-rendering" rel="noopener noreferrer"&gt;Rendex rendering API&lt;/a&gt; and get PNG bytes back. A real, modern Chromium runs on Cloudflare's edge — you don't install, patch, or keep a browser alive, and modern CSS (Flexbox, Grid, web fonts) renders exactly as it would in Chrome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install rendex
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rendex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;rendex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Rendex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
&amp;lt;table style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;border-collapse:collapse;font-family:system-ui;font-size:14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
  &amp;lt;thead&amp;gt;
    &amp;lt;tr style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background:#f1f5f9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
      &amp;lt;th style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:left;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Line item&amp;lt;/th&amp;gt;
      &amp;lt;th style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Amount&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
  &amp;lt;/thead&amp;gt;
  &amp;lt;tbody&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Subscriptions&amp;lt;/td&amp;gt;
        &amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;Professional services&amp;lt;/td&amp;gt;
        &amp;lt;td style=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-align:right;padding:8px 14px&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;€91,400&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rendex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device_scale_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rendered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the table is the only thing on the page, render at &lt;code&gt;device_scale_factor=2&lt;/code&gt; for a crisp 2× image, and add &lt;code&gt;full_page=True&lt;/code&gt; when a long table runs past the viewport. No SDK? The same call over raw HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.rendex.dev/v1/screenshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table.png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "html": "&amp;lt;table style=\"border-collapse:collapse;font-family:system-ui\"&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;",
    "format": "png",
    "deviceScaleFactor": 2,
    "fullPage": true
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to use:&lt;/strong&gt; production report pipelines, anything you deploy to a serverless function or a slim container, and any time you don't want a Chromium binary in your image. The free tier is &lt;strong&gt;500 renders/month, no card&lt;/strong&gt; — &lt;a href="https://rendex.dev/login" rel="noopener noreferrer"&gt;grab a key&lt;/a&gt;. For the full report-to-PDF-deliverable pattern (templating with a &lt;code&gt;data{}&lt;/code&gt; object, embedding the PNG in a generated PDF), see the &lt;a href="https://rendex.dev/recipes/html-financial-report-to-png-python" rel="noopener noreferrer"&gt;Python report-rendering recipe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off, honestly:&lt;/strong&gt; it's a network call and a per-render cost, and the HTML leaves your box. For a sync render Rendex processes it transiently and doesn't store it; if that's a dealbreaker for sensitive data, one of the self-hosted options below keeps everything local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: html2image (lightweight, but you supply Chrome)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/vgalin/html2image" rel="noopener noreferrer"&gt;html2image&lt;/a&gt; is a thin wrapper around the headless mode of a Chrome/Chromium/Edge that is &lt;em&gt;already installed on the machine&lt;/em&gt; — it doesn't bundle one. Maintained (latest 2.0.7, May 2025), MIT, and genuinely the simplest local route when a browser is already present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install html2image   (Chrome/Chromium/Edge must already be installed)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html2image&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Html2Image&lt;/span&gt;

&lt;span class="n"&gt;hti&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Html2Image&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;hti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_str&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; you own keeping that Chrome patched and version-matched across every machine and CI runner, and fonts/emoji are whatever the host browser ships — Linux base images often lack CJK and color-emoji fonts, so you'll install font packages to avoid tofu boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Playwright (the modern self-hosted default)
&lt;/h2&gt;

&lt;p&gt;If you're going to run a browser yourself, &lt;a href="https://playwright.dev/python/docs/screenshots" rel="noopener noreferrer"&gt;Playwright for Python&lt;/a&gt; is the current standard (1.60, May 2026, with evergreen bundled browsers). &lt;code&gt;set_content()&lt;/code&gt; loads your HTML; locate the table and screenshot just that element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install playwright  &amp;amp;&amp;amp;  playwright install chromium
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# crop to the table
&lt;/span&gt;    &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the bundled browsers are hundreds of MB in your image; you patch them; and a long-running process leaks memory and leaves zombie processes if you don't close contexts. Full-page shots over ~16,384px can OOM. Great control — you just operate the browser fleet now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 4: imgkit / wkhtmltoimage (works, but the engine is archived)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/jarrekk/imgkit" rel="noopener noreferrer"&gt;imgkit&lt;/a&gt; wraps the &lt;code&gt;wkhtmltoimage&lt;/code&gt; binary. It's the classic answer to this question, and the one to be most careful with in 2026.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install imgkit   +   system binary: apt-get install wkhtmltopdf
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;imgkit&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;imgkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# headless box: wrap in xvfb-run
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the underlying &lt;code&gt;wkhtmltoimage&lt;/code&gt;/wkhtmltopdf engine was &lt;strong&gt;archived in January 2023&lt;/strong&gt; (last stable release June 2020) and gets no security patches. It's a frozen, old WebKit: modern CSS Flex/Grid, web fonts, and emoji are broken or silently dropped, and on a headless server it throws &lt;code&gt;QXcbConnection: Could not connect to display&lt;/code&gt; unless you wrap every call in &lt;code&gt;xvfb-run&lt;/code&gt;. imgkit itself is effectively unmaintained (last release 1.2.3). Fine for a legacy script; not what you'd start a new project on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 5: Selenium (works, heaviest for one PNG)
&lt;/h2&gt;

&lt;p&gt;Selenium drives a real browser and can screenshot an element. It's maintained and battle-tested, but it's the most boilerplate for "HTML string → one PNG."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pip install selenium   (Selenium Manager fetches a matching ChromeDriver)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;table border=1&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Line item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Amount&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; \
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Subscriptions&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;€482,100&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/table&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--headless=new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:text/html;charset=utf-8,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Where it bites:&lt;/strong&gt; the classic &lt;code&gt;SessionNotCreatedException&lt;/code&gt; when ChromeDriver and the host Chrome drift apart (Selenium Manager mostly handles this now), full-page screenshots aren't first-class, and it's the same browser-fleet ownership as Playwright with more ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on WeasyPrint
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://weasyprint.org/" rel="noopener noreferrer"&gt;WeasyPrint&lt;/a&gt; comes up in every "HTML to image" search, so it's worth being precise: it's an excellent, actively-maintained pure-Python engine — but it renders to &lt;strong&gt;PDF only&lt;/strong&gt; (PNG export was removed in v53) and &lt;strong&gt;runs no JavaScript&lt;/strong&gt;. If your table is static HTML/CSS and you actually want a PDF, WeasyPrint is a great, dependency-light choice and you don't need any of the above. If you need a &lt;strong&gt;PNG&lt;/strong&gt;, or your table/chart is built by JavaScript, it can't do the job without a second PDF→PNG conversion step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you use?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Browser to host?&lt;/th&gt;
&lt;th&gt;PNG?&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rendex API&lt;/td&gt;
&lt;td&gt;No (runs on the edge)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Production pipelines, serverless, no infra to own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;html2image&lt;/td&gt;
&lt;td&gt;Yes (you supply Chrome)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Quick local jobs where a browser is already present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Yes (bundled)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Full browser control, if you want to run the fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imgkit / wkhtmltoimage&lt;/td&gt;
&lt;td&gt;No (archived engine)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Legacy scripts only — engine archived 2023&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selenium&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (viewport)&lt;/td&gt;
&lt;td&gt;Teams already standardized on Selenium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WeasyPrint&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No (PDF only)&lt;/td&gt;
&lt;td&gt;Static HTML → PDF, no JS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest summary: the self-hosted libraries all &lt;em&gt;work&lt;/em&gt; — the real question is whether you want to own a browser (install, patch, scale, debug fonts, watch memory) for what is often a side-task to your actual report logic. If yes, Playwright is the modern pick. If you'd rather keep your deployment a plain Python service and POST HTML to get a PNG back, that's exactly what &lt;a href="https://rendex.dev/use-cases/server-side-report-rendering" rel="noopener noreferrer"&gt;Rendex&lt;/a&gt; is for — 500 free renders a month to prove the pipeline before you scale.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
