webcapture Get a free key

Convert a URL to PDF in TypeScript with fetch

The endpoint's response is a fixed shape, so it deserves a fixed type: one Receipt interface, one POST, one authenticated GET, and the bytes on disk.

The sync receipt is the whole API contract in six fields plus the download url, and typing it turns the endpoint into a function your compiler checks. The same interface serves the dark-mode and markdown recipes, where only the request body changes.

The recipe

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

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

interface Receipt {
  id: string;
  bytes: number;
  attempts: number;
  remaining: number;
  sourceUrl: string;
  sourceType: "url" | "html";
  download: string;
}

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/"}),
});
if (!res.ok) throw new Error(`convert failed: ${res.status}`);
const receipt = (await res.json()) as Receipt;

// receipt.download: ${ORIGIN}/v1/conversions/cnv_9f2c01ab.pdf
const file = await fetch(receipt.download, {
  headers: { "Authorization": "Bearer wc_your_key" },
});
if (!file.ok) throw new Error(`download failed: ${file.status}`);
await writeFile("page.pdf", Buffer.from(await file.arrayBuffer()));
console.log(`${receipt.bytes} bytes, ${receipt.remaining} renders left this month`);

remaining comes straight off the typed receipt and reads as the key's quota position after the render, which makes it the natural field to log alongside bytes. The type matches the sync response exactly; the async ticket from ?wait=false is a different shape, and the polling recipe types that one.

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.