Skip to main content

Output format

A PullMD conversion returns a Markdown body, optional frontmatter, response headers and, with format=json, a JSON object. Errors use one JSON shape.

The body​

Every result starts with the title as a level-1 heading, followed by the content. With the trafilatura extractor, the page's own <h1> appears as a second heading when it differs from the page title (for example Markdown - Wikipedia and Markdown). The source URL, date and metadata are in the frontmatter, which you request separately. A short result:

# Release checklist

Run through these steps before every release so nothing ships half-finished.

## Before tagging

- Update the changelog with every user-facing change.

Tables come out without padding spaces, which saves tokens. Reddit and Hacker News threads add a horizontal rule and a comments section after the post.

Formats​

formatContent-TypeBody
md (default)text/markdownMarkdown
texttext/plainThe same content with heading markers, emphasis and quote markers removed. Links become text (url), images become their URL, and unindented *, + and - bullets become - . Backslash escapes resolve to the bare character. Tables, ordered lists and horizontal rules keep their Markdown. Code blocks aren't protected: the same rules apply inside them, except that their backslash escapes stay.
jsonapplication/jsonThe Markdown and its metadata in one object

JSON​

A format=json response for https://example.com, shortened:

{
"markdown": "# Example Domain\n\nThis domain is for use in documentation examples…",
"metadata": {
"title": "Example Domain",
"description": null,
"author": null,
"publishedTime": null,
"language": "en",
"sourceUrl": "https://example.com",
"statusCode": 200,
"quality": 0.65,
"extractorReason": "low quality (0.2) → rendered via playwright",
"contentLength": 905
},
"source": "playwright",
"shareId": "87e87713d5aa7329c7248a44dc5ed321"
}

The example leaves out some metadata fields: canonical, modifiedTime, and the Open Graph and Twitter card fields (ogTitle, ogImage, twitterCard and so on). A field the page doesn't have is null. contentLength and extractorReason are present for pages extracted by Readability, Trafilatura or Playwright and for POST /api/html. Uploads have contentLength without extractorReason, and Reddit, Hacker News and Cloudflare Markdown results have neither. A cached Reddit or Hacker News result has a shorter metadata object that holds only the fields it stores, such as title, author, published, upvotes, sourceUrl and quality.

On a cache hit, quality is recomputed from the cached Markdown, so it can differ from the value of the fresh fetch.

For POST /api/html and POST /api/file, statusCode is null, and so is sourceUrl for files. With query, an extract object is added. shareId is null for uploads. Every field is described in the API reference.

Frontmatter​

Add frontmatter=true to get a YAML block before the body:

---
title: Markdown - Wikipedia
url: https://en.wikipedia.org/wiki/Markdown
source: readability
fetched: 2026-10-19T00:37:20.456Z
quality: 1
language: en
extractor_reason: tables preserved (33 vs 3)
share_id: 676e61c856410ee32501a44b154fbc31
---

A field appears only when it has a value.

FieldPresent forValue
titleResults with a title. Results from MarkItDown, PDF OCR, a vision or speech model, or YouTube have none, except a URL result served from the cache.Page or document title
urlResults with a source URLSource URL. Most uploaded files and POST /api/html without url have none; saved pages (.mhtml) and feeds carry the URL stated in the file.
sourceAllWhich extractor produced the result. See X-Source values.
fetchedAllWhen this response was built (ISO 8601)
qualityAllExtraction confidence, 0 to 1
authorPages, Reddit, Hacker News, YouTube, and uploads of e-mail, OpenDocument, RTF and feedsAuthor, poster or sender. Reddit values start with u/.
publishedPages, Reddit, Hacker News, YouTube, and uploads of e-mail, OpenDocument and RTFPublication date. ISO 8601 for Reddit, Hacker News and e-mail; other sources give the date as the source states it.
modifiedPagesLast-modified date from the page
descriptionPagesMeta or Open Graph description
languagePagesPage language
imagePagesOpen Graph or Twitter image
sitePagesSite name
extractor_reasonPagesWhy this extractor won, or why the page was rendered
share_idCached resultsShare id. Open at /s/<id>.
subreddit, upvotesReddit (upvotes also Hacker News)Post details. subreddit starts with r/.
duration, viewsYouTubeVideo length and view count
image_sizeCaptioned imagesWIDTHxHEIGHT
audio_secondsTranscribed audioLength in seconds
llm_model, llm_tokensCaptions, transcripts and PDF OCRModel and total tokens, when the provider reports usage
llm_prompt_tokens, llm_completion_tokensCaptions, transcripts and PDF OCRToken split, when the provider reports it
pdf_pagesPDF OCRPages processed
images_transcribedReddit posts with imagesTranscribed images as <done>/<total>. See Reddit and Hacker News.
extracted, extract_confidence, sections_selected, original_tokens, returned_tokensRequests with querySee Query-scoped extraction

MCP read_url results always include a frontmatter block, even with frontmatter=false, with source, share_id, share_url, quality and cached. get_share results carry url, source, share_id, share_url, refreshed and age_ms. See MCP tools.

Site recipes can add their own fields.

Keep only some fields​

To trim tokens for agents, set PULLMD_FRONTMATTER_FIELDS to the fields to keep:

PULLMD_FRONTMATTER_FIELDS=title,url,source

Unknown names are ignored with a warning at startup. If none of the names are valid, every field is kept.

Response headers​

HeaderPresentValue
X-SourceEvery conversionWhich extractor produced the result
X-QualityEvery conversion0 to 1
X-Share-IdCached conversions32-character hex share id
X-Suggested-FilenameEvery conversionA file name for saving the result, such as example-domain.md
X-Transcript-StatusFresh YouTube fetchesok, none, blocked or error
X-Extracted, X-Extract-Confidence, X-Extract-Sections, X-Extract-Original-Tokens, X-Extract-Returned-TokensRequests with querySee Query-scoped extraction

/api/stream can't send headers after the stream starts, so the result event carries source, shareId and suggestedFilename instead, plus transcriptStatus for fresh web conversions.

Suggested file names​

YouTube results are named YT-<title>-<video id>.md. Documents, images and audio use the original file name with .md. Everything else uses the title, then the file name in the URL, the share id, or pullmd. File names and titles are lowercased, characters other than letters and digits become hyphens, the result is cut to 60 characters, and the original extension is dropped. To put a date in front, set PULLMD_FILENAME_DATE_PREFIX.

X-Source values​

ValueMeaning
cloudflareThe site served Markdown itself
readabilityMozilla Readability on the static HTML
readability-fallbackReadability couldn't find an article and returned the whole page body
trafilaturaTrafilatura on the static HTML
playwrightRendered in headless Chromium, then extracted
coverage-guardThe coverage guard replaced a result that missed most of the page
article-seriesSeveral short <article> blocks by the same author were joined
recipe-contentA site recipe named the article body
redditReddit API
hackernewsHacker News API
youtubeYouTube transcript
markitdownDocument conversion
pdf-ocrPDF OCR provider
image-captionVision model
audio-transcriptSpeech-to-text model
email, text, markdown, subtitles, opendocument, rtf, calendar, contact, data, feed, notebook, codeAn uploaded file PullMD read itself

Errors​

Errors are JSON with an error message for every format:

{ "error": "Missing required parameter: url" }

/api/stream is the exception: once the event stream has started, an error arrives as an error event whose data holds a message.

StatusWhen
400Missing or invalid parameter, empty upload, or a Reddit page that isn't a post or subreddit
401Sign-in is on and no valid credentials were sent
403URL blocked by SSRF protection, a URL that can't be parsed or isn't http or https on GET /api, or history and deletes refused by DISABLE_PUBLIC_HISTORY
404Share link, cache entry or Reddit post not found
413Upload too large
422Uploaded HTML has almost no content, or an uploaded file doesn't match its format
429Rate limit reached. Share-link lookups send a Retry-After header.
500Unexpected Reddit error, or extraction failed on POST /api/html
502The source or a sidecar failed. On GET /api, a site behind a bot-protection challenge adds "code": "bot_challenge" and the challenge's vendor to the body.