PaperType

Export a Coda page or a whole doc to PDF with the API (Python script)

Coda (renamed Superhuman Docs in 2026; the API still lives at coda.io/apis/v1) can export a page's content as Markdown through its API: begin an export, poll until it is complete, download the file. https://papertype.dev/v1/pdf turns that Markdown into a PDF. This page gives you a script (papertype-coda.py, Python 3.9+, standard library only, MIT) that does the whole chain for one page from its browser link, or for every visible page of a doc as one PDF with a contents page and a chapter per page. The script was executed end to end on 2026-10-24 with the Coda responses replaced by stubs shaped like the API reference and everything on our side live; it was not run against a Coda workspace, so every Coda fact below is quoted from the reference at docs.superhuman.com/developers/apis/v1 (where coda.io/developers redirects) fetched the same day.

Setup: a Coda API token, a PaperType key, one command

Create a free API key at /signup. On the Coda side you need an API token: the reference authenticates every call with Authorization: Bearer <your API token> and says the API is "available to all of our users free of charge, in both free and paid workspaces". Tokens are issued from your account settings; the click path is not in the reference and was not observed here, so check the API settings section of your account. The page link is the one in your browser's address bar; the script asks Coda's Resolve browser link endpoint which doc and page it points at, so you never copy ids by hand.

export CODA_API_TOKEN=...            # from your Coda account settings
export PAPERTYPE_API_KEY=dp_...      # from /signup
curl -sO https://papertype.dev/static/coda/papertype-coda.py

python3 papertype-coda.py "https://coda.io/d/Launch-plan_dAbCDeFGH/Launch-Status_sumnO"
#  Launch-Status.pdf  1 page(s)

python3 papertype-coda.py <link> --theme report --page-size Letter --toc   # options
python3 papertype-coda.py <link> --url     # print a 24-hour download link instead of saving

What the script sends to Coda, straight from the reference (base path https://coda.io/apis/v1):

  • GET /resolveBrowserLink?url=<your link> → {"resource": {"type": "page", "id": "canvas-IjkLmnO", "href": "…/docs/AbCDeFGH/pages/canvas-IjkLmnO"}}; the doc id and page id come out of that href. A link that resolves to the doc itself makes the script tell you to use --doc.
  • GET /docs/{docId}/pages/{pageId} for the page's name.
  • POST /docs/{docId}/pages/{pageId}/export with body {"outputFormat": "markdown"} (the enum is html or markdown) → 202 {"id", "status", "href"}, where href is "the URL that reports the status of this export".
  • GET that status URL every 1.5 s until status is complete, then download downloadLink, which the reference says "typically expires after a short time". The reference's JSON sample spells the final status complete and its Python comment completed; the script accepts both. A failed status or an error field stops the script with Coda's message, and an export that takes more than two minutes does too.

Then one POST to https://papertype.dev/v1/pdf:

{
  "markdown": "# Launch Status\n\n## Where we are\n\nLaunch is **on track** for the week of 2026-11-02.\n\n| Task | Owner | Status |\n|---|---|---|\n| Finalise pricing | Ada | Done |\n\n- ☑ Legal review\n- ☐ Load test\n\n![Launch timeline](data:image/png;base64,…)\n…",
  "title": "Launch Status",
  "theme": "clean",
  "page_size": "A4",
  "toc": false,
  "page_numbers": true,
  "filename": "Launch-Status.pdf",
  "output": "pdf"
}

Live result on 2026-10-24 for the test page (a heading, a paragraph, a table, two task items, two images, a quote): 200, X-Pages: 1, saved as Launch-Status.pdf; the text read back from the PDF had the page name, the table rows, the task items and the quote.

What the script does to the export

The reference documents the export mechanics but not the Markdown dialect, so the script changes as little as possible and every change below is exercised by its test:

  • Page name as the first heading. The export is the page body; the page's name comes from the page object, so the script puts it in as # Name unless the body already starts with it.
  • Images. The renderer has no network access. Images, fonts and stylesheets at external URLs are never fetched; inline them as data: URIs. A request that references an external URL fails with a 422 that names the URL, rather than silently dropping the asset. The API answers 422 External resources are disabled for any remote image URL left in the Markdown (observed while testing), so the script downloads each image and inlines it as a data: URI, up to about 1.4 MB of encoded images per document (2 MB of source per request); an image that is larger, fails to download, or is over budget is printed as a link line with its alt text. --no-images turns every image into such a link without downloading anything.
  • Task lists. - [x] and - [ ] print literally in the renderer, so they become ☑ and ☐ list items.
  • Whitespace only. Windows line endings, trailing spaces and runs of blank lines are normalised. Headings, tables, emphasis, links and code are passed through as Coda exported them; if your export contains a construct that renders badly, send it to support@papertype.dev and the script gets a rule for it.
  • Hidden and non-canvas pages. With --doc, pages whose isEffectivelyHidden is true are skipped (--include-hidden keeps them) and only contentType: canvas pages are exported; the reference lists embed and syncPage as the other types and the script leaves those out.

A whole doc: one PDF with chapters, or one PDF per page

--doc takes the doc's link or id, lists its pages with GET /docs/{docId}/pages?limit=100 (following nextPageToken) and exports each one:

python3 papertype-coda.py --doc "https://coda.io/d/Launch-plan_dAbCDeFGH" --toc
#  2 page(s) in Launch plan
#  ./Launch-plan.pdf  3 page(s)

python3 papertype-coda.py --doc AbCDeFGH --separate --outdir pdfs
#  pdfs/Launch-Status.pdf      1 page(s)
#  pdfs/Risks-mitigations.pdf  1 page(s)

The combined document is built so the contents page is useful: the doc's name is the title, each Coda page becomes a level-2 chapter heading that starts on a new PDF page, and the page's own headings move down one level (the API's contents page lists heading levels 2 and 3, so an H1 per page would not appear in it). Live result on 2026-10-24: a two-page doc with --toc produced a 3-page PDF (contents page listing both chapters, then one page each); --separate wrote two files named after the pages. Page order is Coda's list order, which the reference describes as the doc's page order; sub-pages are listed with their parent reference but flattened here.

Limits that matter

  • Coda rate limits (reference, 2026-10-24, "subject to change at any time"): reading 100 requests per 6 seconds, writing (POST/PUT/PATCH) 10 per 6 seconds, writing doc content 5 per 10 seconds, listing docs 4 per 6 seconds, per user across all docs. Beginning an export is a POST, so a large doc is paced by the write limit; over it Coda returns 429, which the script waits out (Retry-After, else 3 s) and retries three times. Each page costs one POST plus two or more status polls plus one download.
  • Our quota. Free-plan output carries a small papertype.dev footer mark. Paid plans (from $9/month) do not. The free plan is 50 documents a month and the API returns 429 at the limit; --separate on a 60-page doc needs the $9 plan (500 documents). A combined --doc PDF is one document however many pages it has. Failed renders are not counted.
  • 2 MB of source per request, 300 pages per document, 2 concurrent renders per account. A doc's text is far below 2 MB; images are what fill it, hence the inlining budget above. Over 300 pages, split with --separate.
  • Errors. Coda errors surface as Coda <status> on <method> <path>: <message>, using the reference's codes: 401 "The API token is invalid or has expired", 403 "does not grant access to this resource", 404 "could not be located with the current API token" (also for a link to a doc you cannot open), 410 "has been deleted". Ours surface as PaperType <status>: <detail>; a bad key gives PaperType 401: Missing or invalid API key, as the test checked.
  • What is stored. With the default output the PDF is the response and nothing is kept on our side. With --url the finished PDF (never the Markdown) is kept at a public, unguessable link for 24 hours, as the privacy policy describes; the Coda token is sent only to coda.io.
  • Fonts. Installed fonts: DejaVu, Liberation, Noto, Carlito and Caladea. Embed any other font with an @font-face rule whose src is a data: URI.

Questions

Did you test this against Coda?

Not against a workspace. The script file was executed on 2026-10-24 with its `coda()` and `fetch_blob()` functions replaced by stubs returning the shapes the API reference shows (resolveBrowserLink, a doc, a two-cursor page list with a hidden page and an embed page, a 202 export followed by an in-progress poll and a complete poll with a downloadLink, a failed export) and the PaperType half real: the single-page, `--url`, `--doc` and `--doc --separate` requests were posted to the live API, the PDFs saved and read back with pdftotext, the 24-hour link downloaded without a key, and the 401 path checked (33 checks). If a Coda detail is wrong, tell support@papertype.dev and the page gets fixed.

Why not print the page from Coda?

Do that for one page by hand. The script is for a PDF produced by a job rather than a person: a nightly snapshot of a runbook or meeting-notes doc, a whole doc as one file with a contents page and page numbers, your own theme or CSS, Letter or A5 output, or a link you can post into a chat or an email from an automation.

Which Coda tokens and plans work?

The reference says the API is free in free and paid workspaces, and that what a token can read follows the role of its user in each workspace. A page you can open in the browser is a page the token can export; a page you cannot gives 404. Doc-creation rules in the reference (Doc Maker role) do not apply here because the script only reads.

Can it run on a schedule?

Yes; it is a plain Python script with two environment variables, so a cron entry, a GitHub Actions job (see the GitHub Actions guide) or any server can run it. Whether Coda's own automations can call an outbound URL to trigger it was not checked for this page; the reference's automation endpoint is for triggering Coda automations from outside, which is the other direction.

What about tables, embeds and sub-pages?

Tables in the page body arrive in whatever form Coda's Markdown export gives them; pipe tables render as tables (tested), anything else renders as the text Coda emitted. Embed and sync pages are not exported (`contentType` other than `canvas`). Sub-pages are ordinary pages in the list, so `--doc` includes them as chapters; a single-page run exports only the page you linked.

Related

All guides

Free previews carry a watermark and are deleted after 24 hours. Free API output carries a footer mark. The renderer runs no JavaScript and never fetches external URLs. Privacy · Terms · Refunds