Bulk Screenshot and PDF Generation with a Batch API
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.
This guide covers submitting a batch, polling for results, fanning out with a webhook, and knowing when batch beats firing N single requests.
When to Use Batch vs. Single Requests
Single requests are fine for on-demand captures (a user clicks "screenshot", you capture, you display). Batch is the right choice when:
- You have a fixed list of URLs to capture in one job (site audit, competitor scan, monitoring baseline).
- You want a single webhook callback when all jobs are done rather than N separate callbacks.
- You need a shared progress indicator (34 of 48 complete) without building your own state machine.
Batch is URL-only. If you need to render raw HTML payloads, fire those as async single requests with async: true using the REST API.
Plan Limits
Batch size caps are per request, not per day. You can fire multiple batches.
| Plan | Max URLs per batch |
|---|---|
| Free | 5 |
| Starter | 25 |
| Pro | 100 |
| Enterprise | 500 |
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.
Step 1: Submit a Batch
The batch screenshot API accepts a urls array and a shared defaults object for capture settings. Settings in defaults apply to every URL in the batch. Source-type fields (url, html, markdown) are not allowed in defaults and return a 400 if present.
curl -X POST https://api.rendex.dev/v1/screenshot/batch \
-H "Authorization: Bearer rdx_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://example.com",
"https://example.com/pricing",
"https://example.com/about"
],
"defaults": {
"format": "png",
"fullPage": true,
"width": 1280
}
}'
# 202 Accepted — response includes batchId and per-job IDs:
# {
# "batchId": "a3f9c2e1-...",
# "totalJobs": 3,
# "jobs": [
# { "jobId": "job_01jyav...", "url": "https://example.com", "status": "queued" },
# ...
# ]
# }
The JS SDK wraps this in rendex.batch():
import Rendex from "@copperline/rendex"
// npm install @copperline/rendex
const rendex = new Rendex("rdx_YOUR_KEY") // get one at rendex.dev/login
const result = await rendex.batch({
urls: [
"https://example.com",
"https://example.com/pricing",
"https://example.com/about",
],
defaults: {
format: "png",
fullPage: true,
width: 1280,
},
})
console.log(result.data.batchId) // "a3f9c2e1-..."
console.log(result.data.totalJobs) // 3
The Python SDK uses rendex.batch() with snake_case parameter names:
from rendex import Rendex
# pip install rendex
rendex = Rendex("rdx_YOUR_KEY") # get one at rendex.dev/login
result = rendex.batch(
urls=[
"https://example.com",
"https://example.com/pricing",
"https://example.com/about",
],
defaults={
"format": "png",
"fullPage": True,
"width": 1280,
},
)
batch_id = result["data"]["batchId"]
print(f"Submitted {result['data']['totalJobs']} jobs, batch {batch_id}")
Step 2: Poll for Results
The batch runs asynchronously on Cloudflare Queues. Poll GET /v1/batches/:batchId to check progress. Each job in the response includes a resultUrl (signed R2 link, valid for 24 hours by default) once its status is completed.
import Rendex from "@copperline/rendex"
const rendex = new Rendex("rdx_YOUR_KEY")
async function waitForBatch(batchId: string, intervalMs = 2000): Promise<void> {
while (true) {
const status = await rendex.batchStatus(batchId)
const { batch } = status.data
console.log(
`${batch.completedJobs}/${batch.totalJobs} done` +
` (failed: ${batch.failedJobs})`
)
if (batch.status === "completed" || batch.status === "partial") {
for (const job of batch.jobs) {
if (job.status === "completed" && job.resultUrl) {
console.log(job.resultUrl) // signed PNG URL
}
}
break
}
await new Promise((r) => setTimeout(r, intervalMs))
}
}
waitForBatch("a3f9c2e1-...")
The batch status field is one of: processing, completed, partial (some jobs failed), or failed.
Step 3: Receive a Webhook When Done
Polling works for scripts. For production services, pass webhookUrl 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.
curl -X POST https://api.rendex.dev/v1/screenshot/batch \
-H "Authorization: Bearer rdx_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://example.com", "https://example.com/pricing"],
"defaults": { "format": "png", "fullPage": true },
"webhookUrl": "https://your-app.example.com/hooks/rendex-batch"
}'
// Example: Next.js App Router handler
import { NextRequest, NextResponse } from "next/server"
import { verifyWebhookSignature } from "@copperline/rendex/webhooks"
export async function POST(req: NextRequest) {
const body = await req.text()
const sig = req.headers.get("rendex-signature") ?? ""
const valid = verifyWebhookSignature({
body,
signature: sig,
secret: process.env.RENDEX_WEBHOOK_SECRET!,
})
if (!valid) return NextResponse.json({ error: "bad signature" }, { status: 401 })
const payload = JSON.parse(body)
const { batchId, status, jobs } = payload.data
const resultUrls = jobs
.filter((j) => j.status === "completed")
.map((j) => j.resultUrl)
console.log(`Batch ${batchId} settled (${status}): ${resultUrls.length} screenshots ready`)
return NextResponse.json({ received: true })
}
Controlling How Long Result URLs Last
By default, signed result URLs expire after 24 hours. Use the cacheTtl 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).
{
"urls": ["https://example.com"],
"defaults": { "format": "png" },
"cacheTtl": 604800
}
Troubleshooting
BATCH_LIMIT_EXCEEDED (400): 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.
Some jobs show status "failed": Individual jobs can fail without failing the whole batch. Check the error 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.
Webhook not received: 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 cloudflared tunnel or ngrok and poll instead.
PLAN_UPGRADE_REQUIRED on a batch with geo: The geo parameter in defaults requires a Pro or Enterprise plan. Free and Starter batches cannot use geo-targeting.
Next Steps
Batch capture works well alongside scheduled monitoring. The website monitoring with automated screenshots guide shows a Cloudflare Workers cron that fires a batch of key pages hourly and alerts on visual changes.
For the full request schema and all defaults options, see the API reference.
Ready to run your first batch? The free screenshot tool lets you try a single capture without code. Get an API key (100 free calls/month, no credit card) to start submitting batches.

Top comments (0)