Archive web pages and publish Markdown as PDF

Two jobs that look alike and are not. Snapshot a page someone else controls as evidence, and publish documentation your team writes in Markdown as a clean, styled PDF.

The previous lesson assumed you own the HTML. Two common requests break that assumption. Legal or support asks you to keep a PDF copy of a page as it looked on a given day: your own terms of service when a customer accepted them, a partner's pricing page, a regulator's guidance. And your docs team writes in Markdown and wants a downloadable PDF of the handbook without anyone hand-writing HTML.

Website to PDF handles the first: give it a URL, get a PDF. Markdown to PDF handles the second: give it Markdown text, get a PDF. Both return the same kind of response you saw for HTML, a signed downloadURL, an expires time in milliseconds and a pdfName, so the savePdf helper from lesson 1 works unchanged for both.

Archiving a page with Website to PDF

The request takes one required parameter, url, and the same paid-plan layout parameters as HTML to PDF: four margins in inches and landscape. It costs 40 credits, twice an HTML render, because the service has to fetch and load someone else's page before printing it.

Try it liveWebsite to PDF40 credits
POSThttps://api.apiverve.com/v1/websitetopdf
curl -X POST "https://api.apiverve.com/v1/websitetopdf" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com"}'
Sign in or create a free account to run this with your own key.

data.url echoes the address that was rendered; store it next to the file.

The response adds one field you did not get from HTML to PDF: url, the address that was converted. Keep it. A PDF of a web page is only useful as a record if you can say what it is a record of.

What makes a snapshot trustworthy

An archive exists to answer a question later, usually "what did this page say on that date?" A bare PDF file in a bucket does not answer it. Store, alongside every snapshot:

  • the URL you requested and the url the API echoed,
  • the time you captured it, from your own clock, in UTC,
  • a SHA-256 hash of the PDF bytes, so you can show the file has not been edited since,
  • why you captured it (the customer ID that accepted the terms, the ticket number).

The hash is the part people skip and regret. Without it, "this is what the page said" is your word; with it recorded at capture time in a system that keeps history, it is evidence that the file has not changed since.

import { createHash } from 'node:crypto';
import { mkdir, writeFile } from 'node:fs/promises';

const API = 'https://api.apiverve.com/v1';

async function call(api, payload) {
  const res = await fetch(`${API}/${api}`, {
    method: 'POST',
    headers: { 'x-api-key': process.env.APIVERVE_API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(60000),
  });
  const body = await res.json().catch(() => null);
  if (!res.ok || body?.status !== 'ok') throw new Error(`${api}: ${body?.error || `HTTP ${res.status}`}`);
  return body.data;
}

export async function archivePage(url, reason) {
  const capturedAt = new Date().toISOString();
  const data = await call('websitetopdf', { url });
  const pdf = Buffer.from(await (await fetch(data.downloadURL)).arrayBuffer());
  if (pdf.subarray(0, 5).toString() !== '%PDF-') throw new Error('Not a PDF');

  const sha256 = createHash('sha256').update(pdf).digest('hex');
  const dir = `archive/${capturedAt.slice(0, 10)}`;
  await mkdir(dir, { recursive: true });
  await writeFile(`${dir}/${sha256}.pdf`, pdf);
  const record = { requestedUrl: url, renderedUrl: data.url, capturedAt, sha256, bytes: pdf.length, reason };
  await writeFile(`${dir}/${sha256}.json`, JSON.stringify(record, null, 2));
  return record;
}

console.log(await archivePage('https://example.com', 'terms snapshot for ticket 4411'));

Naming the file by its hash has a useful side effect: capture the same unchanged page twice and you write the same file name, so duplicates collapse on their own. The timeout is longer than for HTML because the service is waiting on a third-party site, and a slow site is slow for everyone who loads it.

Budget before you schedule. At 40 credits a snapshot, watching 50 pages weekly is 2,000 credits a week, and most of those snapshots will be identical to last week's. A cheaper pattern is to fetch the page's text first with a text-extraction API or a plain GET, compare it with what you stored last time, and only render a PDF when the content changed. Log every check with its time and the hash of the text you compared, so you can still say the page was unchanged on a date you did not render.

What the renderer can and cannot see

The service loads the page from the public internet, without your cookies, session or VPN. That decides what ends up in the PDF:

  • Pages behind a login render as the login page. There is no way to pass credentials through this API, and you should not want one. If you need a PDF of an authenticated page in your own app, render that page's HTML yourself and use HTML to PDF.
  • Internal hosts (localhost, 10.x addresses, staging behind a firewall) are not reachable from the service.
  • Cookie banners and pop-ups are part of the page, so they may be part of the PDF. For your own pages, hide them with a @media print rule; for third-party pages, accept that the snapshot shows what an anonymous visitor saw, which is often exactly what an archive should show.
  • Lazy-loaded content that only appears on scroll may be missing below the fold. Check a sample before you build a pipeline around a particular site.

Paper size and fonts behave as they did in lesson 1. Our snapshot of example.com came out as an A4 page, the same 595.92 by 842.88 point sheet as an HTML render.

Check yourself

You snapshot a partner's pricing page every Monday. Months later, a dispute turns on what the page said on March 3. Which stored detail does the most to show your PDF has not been altered since capture?

Answer the check above to continue.

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales