Turn a ClickUp task into a PDF, a List into a report with a page-numbered task table, or a Doc into a PDF (Python script)
ClickUp's API returns a task's fields and, with include_markdown_description=true, its description as Markdown; the Docs API returns a Doc's pages as Markdown too. This page gives you a script (papertype-clickup.py, Python 3.9+, standard library only, MIT) that assembles that into a document and posts it to https://papertype.dev/v1/pdf: one task as a PDF with its fields, custom fields, description, subtasks, checklists, attachments and comments; every task in a List as one document with a task table whose page numbers are real, or as one file per task; or a Doc with all its pages and a contents page. The script was executed end to end on 2026-10-26 with the ClickUp responses replaced by stubs shaped like the OpenAPI definitions on developer.clickup.com and everything on our side live (the PDFs were saved and read back); it was not run against a ClickUp Workspace, so every ClickUp fact below is quoted from those reference pages and ClickUp's help centre, fetched the same day, and the places where the docs leave a detail open are named.
Setup: a personal token, a PaperType key, one command
Create a free API key at /signup. On the ClickUp side you need a personal API token (the authentication page, fetched 2026-10-26): "For personal use, authenticate with your personal API token"; in ClickUp click your avatar → Settings → Apps → under API Token click Generate, then Copy. Tokens begin with pk_ and "never expire". The header is the bare token, Authorization: pk_…, no Bearer prefix. OAuth is for "apps or integrations that other people use"; a script you run yourself does not need it. The public API "is available on all ClickUp Plans".
export CLICKUP_API_TOKEN=pk_... # ClickUp → avatar → Settings → Apps → API Token
export PAPERTYPE_API_KEY=dp_... # from /signup
curl -sO https://papertype.dev/static/clickup/papertype-clickup.py
python3 papertype-clickup.py https://app.clickup.com/t/86d2m1awy
# PAY-123-Checkout-fails-with-Apple-Pay.pdf 4 page(s)
python3 papertype-clickup.py 86d2m1awy task.pdf --theme report --footer "Confidential"
python3 papertype-clickup.py --custom-id PAY-123 --workspace 9011234080 # by custom task id
python3 papertype-clickup.py 86d2m1awy --url # print a 24-hour download link instead of savingThe argument is a task id or the task URL from your browser (app.clickup.com/t/<id>). What the script sends to ClickUp, all GET against https://api.clickup.com/api, each quoted from the reference fetched 2026-10-26:
/v2/task/{task_id}?include_markdown_description=true&include_subtasks=true. The reference:markdown_descriptionis "only returned ifinclude_markdown_description=trueis passed";subtaskslikewise needsinclude_subtasks=true; "tasks with attachments will return anattachmentsresponse"; "Docs attached to a task are not returned". For a custom task id addcustom_task_ids=true&team_id=<Workspace id>("the Workspace ID must be provided using theteam_idparameter"), which is what--custom-iddoes./v2/task/{task_id}/comment: "results are returned in reverse chronological order (newest to oldest). By default, the request returns the 25 most recent comments"; the next page is?start=<date of the last comment>&start_id=<its id>, both required together. The script pages until a short page and prints the comments oldest first./v2/list/{list_id}/task?page=N&include_markdown_description=true&…for--list, see below./v3/workspaces/{workspace_id}/docs/{doc_id}/pages?max_page_depth=-1&content_format=text/mdfor--doc, see below.- The attachment URL (
…p.clickup-attachments.com/…) for each image, with the token in the header; see Images.
Then one POST to https://papertype.dev/v1/pdf with the Markdown the script built, the markdown field because the description arrives as Markdown; the table of fields, the checklists and the comments ride along as HTML blocks inside it, which the renderer accepts:
{
"markdown": "# Checkout fails with Apple Pay\n\n<p class='lead'><strong>in progress · High · Assigned to Mia Krystof · Due 2025-10-30 00:00 UTC</strong></p>\n\n<table class='meta-table'><tr><th>Status</th><td>in progress</td></tr>…</table>\n\n<h2>Description</h2>\n\nMain order flow broken.\n\n### Steps to reproduce\n\n1. Open `/checkout`\n…\n\n<h2>Comments (27)</h2>\n\n…",
"css": "table.tasks td.pg { text-align: right; } a.pg::after { content: target-counter(attr(href), page); } …",
"title": "Checkout fails with Apple Pay",
"theme": "clean",
"page_size": "A4",
"page_numbers": true,
"toc": false,
"filename": "PAY-123-Checkout-fails-with-Apple-Pay.pdf",
"output": "pdf"
}Live result on 2026-10-26 for the test task (nine custom fields, a description with a heading, a numbered list, a code block, a table and an inline image, a subtask, a two-item checklist, a dependency, a link, two attachments, 27 comments): 200, 4 pages with the report theme; the text read back from the PDF had the name, the status, the assignee, a drop-down field's option name, the steps heading, the code, the table, the subtask, the checklist item, the attachment's file name, the first and the last comment and the footer.
What goes on the page
Title: the task name; one bold line with status, priority, assignees and due date; a table of every field that has a value (status, priority, assignees, creator, created, updated, closed, start and due dates, time estimate and time spent as 2h 30m, points, tags, List, Folder, custom id, parent, watchers) followed by every Custom Field that is set, labelled with its ClickUp name; Description; Subtasks with status and assignee; Checklists as ☑ / ☐ items with a done count; Dependencies and links; an Attachments table (file, size, who, when) with image attachments shown below it; Comments (n) oldest first with author, date and a resolved marker; a footer line with the id and the task URL. Rules, each exercised by the script's test:
- ClickUp's Markdown, as is. The description is the
markdown_descriptionClickUp returns, passed through with three changes: its headings are demoted so they sit under the Description heading (a# Planin the description prints as a level-3 heading), images are handled as below, and<script>,<style>andon*=attributes are removed. Tables, code blocks, quotes and lists render as the renderer's Markdown does. When a task has nomarkdown_descriptionthe script falls back totext_content, then prints No description. - Custom Fields by type (the Custom Fields page, fetched 2026-10-26):
drop_downprints the option's name, matched byorderindexor id (the reference's example returns"value": 2for a field whose options carryorderindex0–5);labelsmaps ids to theirlabel;usersprints user names;dateprints the Unix-millisecond value as a UTC date, with the time only whenvalue_options.timeis true;checkboxYes/No;currencythe number and thecurrency_type;number,short_text,text,url,email,phoneas text;emojias4 / 5; progress fields as a percentage;tasksby name;locationby its formatted address. Unset fields are not rows; an unknown shape prints its JSON truncated so nothing is silently dropped. Only the Custom Fields "applicable to the task'scustom_item_id" are in the response, so a task type's own fields appear on its tasks and nowhere else. - Values. ClickUp sends dates as Unix milliseconds in strings (
"1567780450202"); the script prints them as2019-09-06 14:34 UTC. Priority is the object's name (high→ High), or the numeric id by ClickUp's fixed scale (1 Urgent, 2 High, 3 Normal, 4 Low). Status is the status name as your Workspace spells it. A task name that contains Markdown punctuation (Fix *all* the_bugs) prints literally. - Shapes the OpenAPI document leaves open.
checklists,linked_tasksandgroup_assigneesare typed as arrays of objects with no properties listed. The script reads checklists as{name, items: [{name, resolved, assignee, orderindex}]}and linked tasks as{task_id, link_id}, printing the other task's id as a link; if your response differs, the symptom is a missing checklist or link section, not a failed render, and tell us. - Not included. Time entries, the task's activity history, Docs attached to the task (the reference says they are not returned), and the names of linked or dependent tasks (only their ids are in the response; one GET per linked task would fetch them, and the function to extend is
task_sections).
A whole List: --list
--list <list id> renders every task in a List. The default is one document: the title (--title, else the List's name), the count, a task table (id, name, status, priority, assignees, due date) whose last column is the page each task starts on, then a section per task on a new page with the same content as a single-task PDF. The page numbers are not guessed: the table's rows link to each section's anchor and the CSS the script sends uses target-counter(attr(href), page), which the renderer resolves at layout time; the test read the first page back and checked that the three numbers ascend and start at 2.
python3 papertype-clickup.py --list 901234567 --title "Sprint 12" sprint.pdf
# sprint.pdf 7 page(s)
python3 papertype-clickup.py --list 901234567 --table-only --include-closed done.pdf # the table alone, closed tasks too
python3 papertype-clickup.py --list 901234567 --comments --subtasks sprint.pdf # sections with comments; subtasks as rows
python3 papertype-clickup.py --list 901234567 --separate --outdir pdfs # one file per task
# 3 task(s)
# pdfs/PAY-123-Checkout-fails-with-Apple-Pay.pdf 4 page(s)
# …What the reference says about GET /v2/list/{list_id}/task, and what the script does with it: "responses are limited to 100 tasks per page", page "starts at 0" and the response carries last_page, so the script asks page after page until it is true; the endpoint "only includes tasks where the specified list_id is their home List" (tasks added to the List from elsewhere need include_timl, which the script does not send); closed tasks are excluded unless include_closed=true (--include-closed), subtasks unless subtasks=true (--subtasks), and archived=true (--archived) switches to archived tasks. The List id is in the List's URL in ClickUp (not checked for this page; the reference calls it list_id). The List response does not include subtasks, checklists, attachments or comments for each task, so the combined document shows what the List endpoint returns (fields, custom fields, description) plus comments with --comments (one request per task); --separate reads each task in full with GET /v2/task/{id} and gives it everything the single-task PDF has.
Docs: --doc
--doc renders a ClickUp Doc: the Doc name as the title, every page as a heading at its depth (sub-pages nested under their parent), each top-level page starting on a new sheet, the page content as ClickUp's Markdown with its headings demoted below the page heading, and the renderer's contents page in front with page numbers. --page <page id> renders one page only. The Doc id and Workspace id come from the Doc's URL in the browser (app.clickup.com/<workspace>/v/dc/<doc>/<page> is the shape the script accepts; it is not documented in the API reference, so the ids work too), from --workspace / CLICKUP_WORKSPACE_ID, or, when the token can see exactly one Workspace, from GET /v2/team ("Team" is the v2 word for Workspace, the terminology page says).
python3 papertype-clickup.py --doc https://app.clickup.com/9011234080/v/dc/8cht190-831 handbook.pdf
# handbook.pdf 3 page(s)
python3 papertype-clickup.py --doc 8cht190-831 --workspace 9011234080 --page 8cht190-271 page.pdf # one page, no contents pageWhat the Docs reference says (fetched 2026-10-26): GET /v3/workspaces/{workspace_id}/docs/{doc_id}/pages takes max_page_depth ("use -1 to return pages and subpages with no depth limit") and content_format, "text/md for markdown or text/plain for plain text"; each page has name, sub_title, content, pages (its sub-pages), and deleted/archived flags, which the script skips. The Get page entry warns that "due to markdown format limitations, some content elements will not be displayed exactly as they appear in ClickUp", and the Docs API limitation page lists them: headings 1–4, bulleted and numbered lists, quotes, bold, italic, strikethrough, inline code, links, dividers and tables ("loses formatting") come through; code blocks come through "but code formatting is lost"; toggle lists, checklists, banners, columns, synced content, tables of contents, text and background colours, badges, underline, indent, every embed (YouTube, Loom, Google Drive, embedded tasks and Docs), covers, icons, comments, page styles and wikis do not. So a Doc that leans on those prints without them, and this is a ClickUp limit, not ours.
ClickUp exports Docs itself: the help article Export a Doc (updated 2026-10-14) says exports are "available on all ClickUp plans", "everyone, including guests, can export Docs", formats are HTML, PDF, Markdown and Print, for "This Page or All Pages", and "embedded content is not included". For one Doc, once, click that button. The script is for the shape the button does not cover: a Doc exported on a schedule or by a job, in the same theme as your task PDFs, with the contents page, or a Doc and its tasks in one run.
Images, limits, errors
- Images. The renderer never fetches from the web, so every image has to travel inside the request. The script inlines images that live on ClickUp's hosts (
*.clickup-attachments.com,*.clickup.com) asdata:URIs, fetched with your token in the header: a Markdown image in a description or a Doc page, a raw<img>, and the image attachments from the task's attachment list (shown under the attachments table). The budget is 1.2 MB of image bytes per document (about 1.6 MB as base64, under the 2 MB request limit); past it, and for any image on another host, the image becomes a link labelled[image: name]and the script prints how many it linked.--no-imageslinks all of them and fetches nothing. Non-image attachments are listed, never fetched. Not observed first-hand: whether attachment URLs accept the token or are signed public links (the reference shows the URL shape only); either way works with the script, and if a fetch fails the image becomes a link, not an error. - ClickUp rate limits (rate-limits page, 2026-10-26): per token per minute, "100" on Free Forever, Unlimited and Business, "1,000" on Business Plus, "10,000" on Enterprise; over the limit the API returns
429withX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset("Unix timestamp format"). The script waits until the reset time (else 10 s) and retries three times. One task costs 1 request plus one per 25 comments plus one per inlined image; a List costs one request per 100 tasks, plus one per task with--comments, or one full read per task with--separate. A nightly--separateover a 90-task List stays under 100 requests only without comments; add--no-commentsor run it in two Lists. - Our quota. Free-plan output carries a small
papertype.devfooter mark. Paid plans (from $9/month) do not. The free plan is 50 documents a month and the API returns429at the limit; a nightly--separateover 60 tasks needs the $9 plan (500 documents). A combined--listdocument, or a whole Doc, is one document however many tasks or pages it holds. Failed renders are not counted. - 2 MB of source per request, 300 pages per document, 2 concurrent renders per account. Images are the only thing that approaches the 2 MB request limit, hence the 1.2 MB image budget; a long description is kilobytes.
- Errors. ClickUp's errors surface as
ClickUp <status> on GET <path>: <message> (<code>); the common-errors page says a failed request includes "a JSON error message and error code", and the script prints theerrandECODEkeys when present (a revoked token isOAUTH_019/OAUTH_021/OAUTH_025/OAUTH_077, "Token not found"; a Workspace the token cannot see isOAUTH_023–OAUTH_045, "Team not authorized"). Ours surface asPaperType <status>: <detail>; a bad key givesPaperType 401: Missing or invalid API key. - What is stored, and where the token goes. With the default output the PDF is the response and nothing is kept on our side. With
--urlthe finished PDF (never the task data as text) is kept at a public, unguessable link for 24 hours, as the privacy policy describes. The ClickUp token is sent only toapi.clickup.comand ClickUp's attachment hosts; what reaches us is the Markdown the script built, over TLS, inlined images included. The script never writes to ClickUp. - Fonts. Installed fonts: DejaVu, Liberation, Noto, Carlito and Caladea. Embed any other font with an
@font-facerule whosesrcis adata:URI.
Questions
Did you test this against ClickUp?
Not against a Workspace. The script file was executed on 2026-10-26 with its `clickup()` and `fetch_bytes()` functions replaced by stubs returning the shapes the OpenAPI definitions on developer.clickup.com give (a task with `markdown_description`, `custom_fields` of nine types, `subtasks`, `attachments`, `dependencies`; comments in two pages of 25 and 2 with `start`/`start_id`; a List in two pages with `last_page`; a Doc with nested `pages` and `content`; `/v2/team`) and the PaperType half real: the single-task render, the combined `--list` document, `--table-only`, `--separate`, `--doc`, `--doc --page` and `--url` were posted to the live API, the PDFs saved and read back with pdftotext, the task table's page numbers and the Doc's contents page checked, the 24-hour link downloaded without a key, and the 401/404 paths checked (55 checks). The details the docs do not show (checklist and linked-task item shapes, attachment fetch auth, the Doc URL shape) are named above and handled so a mismatch degrades to a missing section or a linked image rather than a failed run. If a ClickUp detail is wrong, tell support@papertype.dev and the page gets fixed.
ClickUp already exports. Why this?
For Docs, ClickUp's own export button (HTML, PDF, Markdown, Print; all plans; one page or all pages) is the right tool for a one-off, and the help article is quoted above. For tasks, the help centre's export articles fetched on 2026-10-26 describe CSV exports of task data (Settings → Imports / Exports) and CSV or Excel exports of List and Table views (Customize → Export); I found no task-to-PDF export in them, and the view exports are spreadsheets, not documents. The script fills that gap: a readable PDF of one task with its description, checklists, attachments and comments, or of a whole List with a page-numbered table, produced by a job rather than a click, in a consistent theme, from one person's read-only token.
Custom task ids, custom task types, Custom Fields scoped to a type?
Custom ids: `--custom-id PAY-123 --workspace <id>`, which sends `custom_task_ids=true&team_id=<id>` as the reference requires; the custom id is also used in file names and the task table when a task has one. Custom task types need nothing: a task's `custom_item_id` selects which Custom Fields ClickUp returns ("only includes Custom Fields applicable to the task's `custom_item_id`"), and the script prints whatever comes back under the field's ClickUp name. Statuses print by name, so custom statuses need nothing either.
Can a ClickUp Automation trigger it?
Not directly: an automation would have to call a URL that runs the script for the task id it is sent, which means a small webhook receiver on your side (the form-submission guide shows a 30-line Python one you can adapt; ClickUp's Automation webhook action and the payload it sends were not checked for this page, though the API's Webhooks pages document task event payloads with a `task_id`). On a schedule, cron or a GitHub Actions job with the two environment variables as secrets is the simplest shape; the GitHub Actions guide has the workflow skeleton.
Which ClickUp plan do I need?
Any: the API-availability page says "our Public API is available on all ClickUp Plans", and the endpoints the script uses (task, comments, List tasks, Docs, Workspaces) are not among the Enterprise-only ones it lists (user and guest management). What differs by plan is the rate limit (100 requests a minute per token on Free Forever, Unlimited and Business), which bounds how many tasks a single run can read; see the limits above.
Related
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