Convert a URL to PDF with Node.js and an asynchronous API
Submit a URL, poll its render job, and download a validated PDF with Node.js. Includes Letter paper, print media, and bounded polling.
By the RenderLark team · 7 min read · Updated
Separate job creation from PDF download
An asynchronous workflow is a good fit for backend jobs and customer-facing applications: submit the page, keep the returned job ID, and check its progress without holding one long request open. RenderLark returns HTTP 202 when a job is accepted; acceptance is not the same as a finished PDF.
This example uses Node.js 22 or newer with its built-in fetch, plus an API key from your RenderLark account. Run it on your server, not in browser JavaScript. Set RENDERLARK_API_KEY in the environment and save the script as render.mjs. The output path is fixed deliberately so untrusted request parameters cannot choose arbitrary files on your server.
Submit, poll, verify, and save
Use POST /api/v1/renders with a Bearer token and exactly one source field: url or html. The example asks for Letter paper and print media. preferCSSPageSize: false makes the requested format take priority over a page's own CSS paper size.
The polling deadline limits this script to two minutes. A local deadline does not cancel the server job: persist the returned ID and resume polling later rather than submitting another job. For production, keep that ID in your application's job table as soon as creation succeeds.
JavaScript · render.mjs
import { writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import { setTimeout as delay } from 'node:timers/promises';
const origin = 'https://renderlark.com';
const apiKey = process.env.RENDERLARK_API_KEY;
if (!apiKey) throw new Error('Missing RENDERLARK_API_KEY');
const headers = { Authorization: 'Bearer ' + apiKey };
async function jsonRequest(path, options = {}) {
const response = await fetch(new URL(path, origin), {
...options,
headers: { ...headers, ...options.headers },
signal: AbortSignal.timeout(15000),
});
if (!response.ok) throw new Error('API HTTP ' + response.status);
return response.json();
}
// Persist this key and reuse it if retrying this creation request.
const requestKey = randomUUID();
const created = await jsonRequest('/api/v1/renders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': requestKey,
},
body: JSON.stringify({
url: 'https://renderlark.com/samples/invoice.html',
options: {
format: 'Letter', media: 'print',
printBackground: true, preferCSSPageSize: false,
},
}),
});
console.log('Keep this job ID:', created.id);
const deadline = Date.now() + 120000;
let completed;
while (Date.now() < deadline) {
const job = await jsonRequest('/api/v1/renders/' + encodeURIComponent(created.id));
if (job.status === 'completed') { completed = job; break; }
if (['failed', 'expired', 'cancelled'].includes(job.status)) {
throw new Error(job.error?.code || 'Render ' + job.status);
}
await delay(3000);
}
if (!completed) throw new Error('Polling paused; resume using the saved job ID');
if (!completed.download_url) throw new Error('Missing download URL');
const downloadUrl = new URL(completed.download_url, origin);
if (downloadUrl.origin !== origin) throw new Error('Unexpected download origin');
const response = await fetch(downloadUrl, {
headers, redirect: 'error', signal: AbortSignal.timeout(30000),
});
if (!response.ok || !response.headers.get('content-type')?.startsWith('application/pdf')) {
throw new Error('Invalid PDF response: HTTP ' + response.status);
}
const pdf = Buffer.from(await response.arrayBuffer());
if (pdf.subarray(0, 5).toString() !== '%PDF-') throw new Error('Invalid PDF signature');
await writeFile('invoice.pdf', pdf, { flag: 'wx', mode: 0o600 });
console.log('Saved invoice.pdf');Handle pages that need more than a basic URL
A renderer fetches the public page from the server. A login prompt, anti-bot challenge, unavailable font, or third-party widget can change the result. A successful PDF response does not prove that a private application's intended invoice was visible; inspect your first documents before automating delivery.
For a page you control, render invoice data on the server and use stable image and font URLs. If a known element appears after client-side work, options.waitFor can specify a selector and a timeout up to 5,000 milliseconds. Avoid pages that depend on a human accepting a cookie banner or completing a login.
Download completed output promptly into your own access-controlled storage. The download link is temporary and should not become a permanent customer attachment URL. Limit your own worker concurrency to the account's allowance, and add the bounded retry policy from our reliability tutorial around transient HTTP failures.