Rendex
Recipe

Batch Screenshot a List of URLs with Webhook Delivery

Fire a list of URLs at one Rendex endpoint and let signed webhooks push each screenshot back to n8n as it finishes — no polling loop, no long-running HTTP request, no queue to babysit.

Last updated 2026-07-21

The Problem

You have a list of URLs to capture on a schedule — a competitor watchlist, every page in a sitemap, a set of customer dashboards. Looping them through a synchronous screenshot call holds an HTTP request open for 5–30 seconds per URL, so a list of 50 either times out the n8n execution or blocks the workflow for minutes. Building your own async layer means a queue, retry logic, idempotency, and a callback receiver — infrastructure that has nothing to do with the screenshots you actually want.

The Solution

Rendex exposes a first-class batch endpoint. Submit the whole URL array in one POST with shared capture defaults and a webhookUrl, get a batchId back immediately, and let Rendex push an HMAC-signed callback to your n8n Webhook node as each job completes. n8n verifies the signature, then routes the resultUrl to storage or a notification — the workflow never blocks waiting on a render.

How the Workflow Runs

Schedule

An n8n Schedule trigger fires the run (e.g. nightly at 02:00).

Read URL list

Pull the URLs from a Google Sheet, Airtable base, or a static list node.

Rendex batch

POST /v1/screenshot/batch with { urls, defaults, webhookUrl } pointing at your n8n Webhook node. Returns a batchId.

Signed callbacks

Your n8n Webhook node receives x-rendex-signature callbacks per job — verify, then store to S3 or notify Slack.

Input → Rendered Output

Left: a JSON array of four URLs (stripe.com, vercel.com, linear.app, railway.app) submitted in a single batch POST. Right: a 2×2 grid of finished screenshot cards, each labeled with its host and a green '200 · signed' status from its HMAC-signed webhook callback.

Rendered by Rendex

What You Need

  • A Rendex API key on the Starter plan or higher — batch is a paid feature (the free tier does single renders). Starter allows 25 URLs per batch, Pro 100, Enterprise 500.
  • An n8n instance with a publicly reachable Webhook node URL (Rendex must be able to POST to it from the open internet).
  • A URL list source: a Google Sheets node, an Airtable node, or a static Set/Code node holding the array.
  • A shared webhook secret you store in n8n credentials and use to verify the x-rendex-signature HMAC on every callback.

What This Recipe Uses

One batch request

POST /v1/screenshot/batch takes a urls array (1–500) plus a single defaults object applied to every capture — no per-URL loop.

HMAC-signed callbacks

Each job POSTs to your webhookUrl with x-rendex-signature, x-rendex-event, x-rendex-timestamp, and x-rendex-delivery-id headers. Verify before you trust the resultUrl.

Async, never blocks

The batch returns a batchId right away. Your n8n run finishes in milliseconds while Rendex renders in the background and pushes results as they land.

Built-in retries

Webhook delivery retries up to 3 times (10s timeout each) on failure, so a brief blip on your n8n endpoint will not silently drop a result.

Build It

batch-submit.sh
# Submit a list of URLs in one request. Each capture inherits "defaults".
# Rendex POSTs an HMAC-signed callback to webhookUrl as each job completes.
curl -X POST https://api.rendex.dev/v1/screenshot/batch \
  -H "Authorization: Bearer $RENDEX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://example.com/pricing",
      "https://example.com/blog",
      "https://example.com/changelog"
    ],
    "defaults": {
      "format": "png",
      "fullPage": true,
      "width": 1280,
      "blockCookieBanners": true
    },
    "webhookUrl": "https://your-n8n.example.com/webhook/rendex-batch"
  }'

# => { "batchId": "btc_01HX...", "jobs": [ { "jobId": "job_01..." }, ... ] }
# Batch is Starter+. Starter 25 / Pro 100 / Enterprise 500 URLs per request.
callback-payload.json
// Rendex POSTs one of these to your webhookUrl per finished job, plus a
// final "batch.completed" summary. Headers carry the auth signal:
//   x-rendex-event:       "job.completed" | "job.failed" | "batch.completed"
//   x-rendex-signature:   HMAC-SHA256 over `${timestamp}.${rawBody}`
//   x-rendex-timestamp:   unix seconds (matches the value signed above)
//   x-rendex-delivery-id: uuid (use for idempotency / dedupe)
{
  "event": "job.completed",
  "batchId": "btc_01HX...",
  "jobId": "job_01HX...",
  "status": "completed",
  "resultUrl": "https://cdn.rendex.dev/r/01HX....png",
  "metadata": { "url": "https://example.com/pricing", "format": "png" },
  "completedAt": "2026-05-28T02:00:14.220Z"
}

// Final summary event for the whole batch:
// {
//   "event": "batch.completed", "batchId": "btc_01HX...",
//   "status": "completed", "totalJobs": 3,
//   "completedJobs": 3, "failedJobs": 0,
//   "completedAt": "2026-05-28T02:00:31.004Z"
// }
verify-rendex-webhook.js
// n8n Code node placed right after the Webhook node.
// Rejects forged callbacks before you store or notify on anything.
const crypto = require('crypto');

const SECRET = $env.RENDEX_WEBHOOK_SECRET; // set in n8n env / credentials

// The Webhook node must run in "raw body" mode so the bytes match what Rendex
// signed. Pull the raw string + headers off the incoming item:
const headers   = $input.first().json.headers;
const rawBody   = $input.first().json.body;          // exact bytes as received
const signature = headers['x-rendex-signature'];
const timestamp = headers['x-rendex-timestamp'];

// Rendex signs `${timestamp}.${rawBody}` with HMAC-SHA256:
const expected = crypto
  .createHmac('sha256', SECRET)
  .update(`${timestamp}.${rawBody}`)
  .digest('hex');

const ok =
  signature &&
  signature.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

if (!ok) {
  throw new Error('Invalid Rendex webhook signature — rejecting callback.');
}

// Optional: reject stale callbacks (replay protection).
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (ageSeconds > 300) {
  throw new Error('Stale Rendex webhook (>5 min) — rejecting.');
}

const payload = JSON.parse(rawBody);
return [{ json: payload }]; // -> route resultUrl to S3 / Slack / DB downstream

100 free API calls/month — no credit card required. Get your API key and start building.

Reach for this workflow whenever you have more than a handful of URLs to capture and you do not want the run to block while they render. A nightly competitor watchlist, a full sitemap audit, a set of tenant dashboards you screenshot for a weekly report — all of them are lists, and lists belong in a batch. The pattern matters because synchronous calls couple your n8n execution time to the slowest page on the list; one heavy URL stalls the whole run. Batching decouples them: you submit once, n8n moves on, and each finished capture arrives as its own signed callback you can route independently to S3, Slack, or a database. The HMAC signature is the part people skip and regret — without verifying x-rendex-signature, any host that learns your webhook URL can forge job.completed payloads. Batch processing is a Starter-plan feature (the free tier is single-render); the batch size cap — 25 URLs on Starter, up to 500 on Enterprise — scales with your plan.

Frequently Asked Questions

Related Resources