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.
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"}'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
urlthe 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'));import hashlib
import json
import os
from datetime import datetime, timezone
import requests
API = "https://api.apiverve.com/v1"
def call(api, payload):
res = requests.post(f"{API}/{api}", json=payload,
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]}, timeout=60)
body = res.json()
if res.status_code != 200 or body.get("status") != "ok":
raise RuntimeError(f"{api}: {body.get('error') or res.status_code}")
return body["data"]
def archive_page(url, reason):
captured_at = datetime.now(timezone.utc).isoformat()
data = call("websitetopdf", {"url": url})
pdf = requests.get(data["downloadURL"], timeout=60).content
if not pdf.startswith(b"%PDF-"):
raise RuntimeError("Not a PDF")
sha256 = hashlib.sha256(pdf).hexdigest()
folder = f"archive/{captured_at[:10]}"
os.makedirs(folder, exist_ok=True)
with open(f"{folder}/{sha256}.pdf", "wb") as f:
f.write(pdf)
record = {"requestedUrl": url, "renderedUrl": data["url"], "capturedAt": captured_at,
"sha256": sha256, "bytes": len(pdf), "reason": reason}
with open(f"{folder}/{sha256}.json", "w") as f:
json.dump(record, f, indent=2)
return record
if __name__ == "__main__":
print(archive_page("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.xaddresses, 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 printrule; 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.
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.
Choosing between the two routes for Markdown
You can also convert Markdown to HTML first (APIVerve has a Markdown to HTML API, or you can use any Markdown library) and send the result to HTML to PDF. Pick by what you need:
| Need | Markdown to PDF | Markdown to HTML, then HTML to PDF |
|---|---|---|
| Fewest moving parts | One call | Two steps |
| Custom margins or landscape | Not available | Paid-plan parameters on HTML to PDF |
| Full template around the content (cover page, header block) | Possible with inline HTML, gets messy | Natural: wrap the HTML in your template |
| Credits per document | 30 | 20 for the PDF, plus whatever the HTML step costs |
For a handbook that is mostly prose, the one-call route is fine. For a branded report where the Markdown is just the body inside a designed template, convert to HTML and use the lesson 1 pipeline.
Next: files you did not write in HTML at all
Web pages and Markdown are text formats a browser understands. Office documents are not: a DOCX contract or an XLSX price list has to be laid out by an office suite. That is the next lesson, /academy/generate-pdfs/03-office-files-to-pdf.