webcapture Get a free key

Download a rendered PDF in Node with fetch

The download url is not a shareable link. It answers to the key that made the render, and this recipe is the contract: who gets bytes, who gets 401, who gets 404.

Every other recipe on this site ends with an authenticated GET. This one is that GET, spelled out: the receipt's download url fetched with the key, the same url fetched without it, and the facts a file server or a background worker needs before it treats the url as a link.

The recipe

import { writeFile } from "node:fs/promises";

const ORIGIN = "https://webcapture.io";

// 1. render: the POST answers with the receipt, not the bytes
const res = await fetch(`${ORIGIN}/v1/convert`, {
  method: "POST",
  headers: {
    "Authorization": "Bearer wc_your_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"url": "https://example.com/"}),
});
const receipt = await res.json();

// receipt.download: ${ORIGIN}/v1/conversions/cnv_9f2c01ab.pdf
// 2. download: the same header goes on the GET
const file = await fetch(receipt.download, {
  headers: { "Authorization": "Bearer wc_your_key" },
});
// 200, application/pdf
console.log(file.status, file.headers.get("content-type"));
await writeFile("page.pdf", Buffer.from(await file.arrayBuffer()));

// 3. what a downloader without the key gets: 401
const stranger = await fetch(receipt.download);
console.log(stranger.status);

Three outcomes, all by design: your key fetches the bytes (200); no key or a revoked key gets 401; a different valid key gets 404, because the id is looked up per key and another key's renders are invisible. The url is therefore a short-lived pointer, not storage: fetch the bytes and keep them yourself.

Same-key GET: 401 without it; 404 for another key.

The fields this recipe uses

Related

Method

Source: convert field reference, checked 2026-10-05. Corrections.