FetchMD API
Documentation
The FetchMD API converts any public URL to clean, LLM-ready Markdown in a single POST request. Use it in RAG pipelines, AI agents, Claude Desktop via MCP, or any code that needs web content without HTML noise.
Quick Start
Get your first URL converted to Markdown in under 60 seconds. No account required for the free tier — just grab an API key and go.
Get a free API key
fetchmd.dev/api-key — enter your email, copy the key. Takes 10 seconds.
Make your first request
curl -X POST https://fetchmd.dev/api/convert \
-H "Content-Type: application/json" \
-H "X-API-Key: fmd_your_key_here" \
-d '{"url":"https://example.com/article"}'Response:
{
"markdown": "# Getting Started with Stripe\n\nStripe is a suite of...",
"url": "https://docs.stripe.com/get-started",
"title": "Getting Started with Stripe",
"tokenEstimate": 1843,
"fetchTimeMs": 312,
"htmlSize": 94200,
"markdownSize": 7372,
"compressionRatio": 92.2
}That's it. The markdown field contains your clean, LLM-ready content. The free tier gives you 100 conversions/month. Upgrade for more →
Authentication
FetchMD uses API keys for authentication. Include your key in every request using the X-API-Key header. Without a key, requests are rate-limited by IP.
curl -X POST https://fetchmd.dev/api/convert \
-H "Content-Type: application/json" \
-H "X-API-Key: fmd_your_key_here" \
-d '{"url":"https://example.com"}'You can also pass the key as a Bearer token:
-H "Authorization: Bearer fmd_your_key_here"Getting an API Key
Use the /api/keys endpoint or visit fetchmd.dev/api-key. Keys are tied to your email address — calling the endpoint again with the same email returns your existing key.
curl -X POST https://fetchmd.dev/api/keys \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com"}'{
"key": "fmd_a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7",
"existing": false
}POST /api/convert
Converts a public URL to clean Markdown. This is the core endpoint — everything else is built around it.
Request body
urlstringrequiredThe URL to fetch and convert. Must include protocol (https:// or http://). The page must return text/html content.
Response body
markdownstringThe full page content converted to clean Markdown. Headings, links, code blocks, tables, and lists are preserved. Navigation, ads, scripts, and boilerplate are stripped.
urlstringThe final URL after redirects are followed.
titlestringThe page title extracted from the <title> tag.
tokenEstimatenumberEstimated token count for the markdown output (characters / 4). Useful for context window planning.
fetchTimeMsnumberTime in milliseconds to fetch the page. Does not include conversion time.
htmlSizenumberOriginal HTML size in bytes.
markdownSizenumberMarkdown output size in bytes.
compressionRationumberPercentage reduction in size from HTML to Markdown. Typically 80–95% for content-heavy pages.
Limits
- · Fetch timeout: 10 seconds per request
- · Only
http://andhttps://protocols - · Target must return
text/htmlcontent-type - · Pages behind login or paywalls cannot be fetched
- · JavaScript-rendered content (SPAs) may return partial content
POST /api/keys
Generates a free API key for the given email address. Idempotent — calling again with the same email returns the existing key.
Request body
emailstringrequiredYour email address. Used to look up your existing key if you call this endpoint again. We don't send marketing emails.
Response body
keystringYour API key. Format: fmd_ followed by 48 hex characters. Store this securely — it won't be shown again via API.
existingbooleantrue if this email already had a key (returned the existing one). false if a new key was created.
POST /api/youtube
Extracts the full transcript from any YouTube video and returns it as clean Markdown. Works with any public video that has captions — manual or auto-generated. No API key required, counts against quota when keyed.
Request body
urlstringrequiredYouTube video URL. Supports: youtube.com/watch?v=, youtu.be/, youtube.com/shorts/, youtube.com/embed/. Or pass an 11-character video ID directly.
timestampsbooleanInclude timestamps in the transcript. Default: true. Set to false for clean paragraph output (better for LLMs).
Response
markdownstringFull transcript as Markdown: title, channel, description, and timestamped or paragraph transcript.
videoIdstringThe 11-character YouTube video ID.
titlestringVideo title.
channelNamestringChannel name.
languagestringLanguage code of the selected transcript track (e.g. en).
segmentCountnumberNumber of caption segments parsed.
durationSecondsnumberVideo duration in seconds.
tokenEstimatenumberEstimated token count for the markdown output.
curl -X POST https://fetchmd.dev/api/youtube \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","timestamps":false}'{
"markdown": "# Never Gonna Give You Up\n\n**Source:** https://www.youtube.com/watch?v=dQw4w9WgXcQ\n**Channel:** RickAstleyVEVO\n\n## Transcript\n\nWe're no strangers to love...",
"videoId": "dQw4w9WgXcQ",
"title": "Rick Astley - Never Gonna Give You Up (Official Video)",
"channelName": "RickAstleyVEVO",
"language": "en",
"segmentCount": 342,
"durationSeconds": 212,
"tokenEstimate": 1840,
"markdownSize": 7362
}POST /api/pdf
Extracts text from any PDF URL and returns it as Markdown. Works with text-based PDFs — research papers, documentation, reports. Not suitable for scanned/image-only PDFs (use an OCR service for those).
Request body
urlstringrequiredDirect link to a PDF file. Must return application/pdf content-type (or include .pdf in the path). Maximum 10MB.
Response
markdownstringExtracted text as Markdown with document title, author, and page count header.
titlestringPDF title from document metadata, or filename if not set.
authorstringAuthor from PDF metadata (may be empty).
pagesnumberDetected page count.
pdfSizenumberOriginal PDF size in bytes.
markdownSizenumberMarkdown output size in bytes.
compressionRationumberPercentage size reduction. Typically 80–95% for text-heavy PDFs.
tokenEstimatenumberEstimated token count for the markdown output.
curl -X POST https://fetchmd.dev/api/pdf \
-H "Content-Type: application/json" \
-d '{"url":"https://arxiv.org/pdf/2303.08774"}'{
"markdown": "# GPT-4 Technical Report\n\n**Source:** https://arxiv.org/pdf/2303.08774\n**Author:** OpenAI\n**Pages:** 98\n\n---\n\nWe report the development of GPT-4...",
"title": "GPT-4 Technical Report",
"author": "OpenAI",
"pages": 98,
"pdfSize": 1842034,
"markdownSize": 184203,
"tokenEstimate": 46050,
"compressionRatio": 90.0
}POST /api/github
Converts a GitHub repository (or specific file/directory) to Markdown. Returns repo metadata, file tree, and README by default. Optionally includes source file contents.
Request body
urlstringrequiredGitHub URL. Supported formats: github.com/owner/repo, github.com/owner/repo/tree/branch, github.com/owner/repo/blob/branch/path/to/file.
includeCodebooleanFetch and include source file contents in the output. Default: false. When true, fetches text files up to 50KB each, up to maxFiles files.
maxFilesnumberMax number of source files to include when includeCode is true. Default: 20, max: 50.
Response
markdownstringRepo overview as Markdown: header, file tree, README, and optionally source files.
ownerstringRepository owner login.
repostringRepository name.
descriptionstringRepository description.
starsnumberStar count.
languagestringPrimary programming language.
defaultBranchstringDefault branch name.
fileCountnumberTotal number of files in the repository.
tokenEstimatenumberEstimated token count.
curl -X POST https://fetchmd.dev/api/github \
-H "Content-Type: application/json" \
-d '{"url":"https://github.com/vercel/next.js","includeCode":false}'{
"markdown": "# vercel/next.js\n\n> The React Framework for the Web\n\n**Stars:** 127,000...",
"owner": "vercel",
"repo": "next.js",
"description": "The React Framework for the Web",
"stars": 127000,
"language": "JavaScript",
"defaultBranch": "canary",
"fileCount": 4821,
"tokenEstimate": 12300
}POST /api/bulk
Converts multiple URLs in a single synchronous request. Results are returned in order — failed URLs include an error field instead of markdown. Requires an API key. For batches over ~20 URLs, use async jobs to avoid timeouts.
Request body
urlsstring[]requiredArray of public HTTP/HTTPS URLs to convert. Plan limits: Free 20, Starter 200, Pro 500, Scale 500. Each URL counts as one conversion against your quota.
Response
resultsarrayArray of result objects in input order. Successful: url, markdown, title, tokenEstimate, compressionRatio. Failed: adds error string.
curl -X POST https://fetchmd.dev/api/bulk \
-H "Content-Type: application/json" \
-H "X-API-Key: fmd_your_key_here" \
-d '{"urls":["https://docs.stripe.com/api","https://docs.github.com"]}'POST /api/jobs (Async Batch)
Submits a large batch of URLs for async processing. Returns a jobId immediately (HTTP 202). Poll GET /api/jobs/:id for results. Use this for batches over ~20 URLs. Results expire after 1 hour.
Request body
urlsstring[]requiredArray of URLs to convert. Maximum 200 per job. Requires API key. Quota is reserved upfront.
Submit — returns 202
curl -X POST https://fetchmd.dev/api/jobs \
-H "Content-Type: application/json" \
-H "X-API-Key: fmd_your_key_here" \
-d '{"urls":["https://docs.stripe.com/api","https://docs.github.com","https://docs.anthropic.com"]}'{
"jobId": "job_01jt5k2n8r4p",
"status": "processing",
"totalUrls": 3,
"pollUrl": "/api/jobs/job_01jt5k2n8r4p",
"submittedAt": "2026-03-02T12:00:00.000Z"
}Poll — GET /api/jobs/:id
Include the same X-API-Key header. Status: processing → complete (or failed). Poll every 1–2 seconds.
curl https://fetchmd.dev/api/jobs/job_01jt5k2n8r4p \
-H "X-API-Key: fmd_your_key_here"{
"jobId": "job_01jt5k2n8r4p",
"status": "complete",
"totalUrls": 3,
"completedUrls": 3,
"submittedAt": "2026-03-02T12:00:00.000Z",
"completedAt": "2026-03-02T12:00:03.421Z",
"results": [
{ "url": "https://docs.stripe.com/api", "title": "Stripe API Reference", "tokenEstimate": 8420, "ok": true },
{ "url": "https://docs.github.com", "title": "GitHub Docs", "tokenEstimate": 3210, "ok": true },
{ "url": "https://docs.anthropic.com", "title": "Anthropic", "tokenEstimate": 1840, "ok": true }
]
}GET /api/quota
Returns current quota usage for an API key. Useful for agents to check remaining capacity before a large batch.
curl https://fetchmd.dev/api/quota \
-H "X-API-Key: fmd_your_key_here"{
"plan": "pro",
"used": 1842,
"limit": 10000,
"remaining": 8158,
"resetsAt": "2026-04-01T00:00:00.000Z",
"batchLimit": 500
}Errors
All errors return JSON with an error field describing what went wrong. HTTP status codes follow standard conventions.
| Code | Name | Description |
|---|---|---|
400 | Bad Request | Missing or invalid url field, or invalid email for key generation. |
401 | Unauthorized | Invalid or unrecognized API key. Get one at fetchmd.dev/api-key. |
422 | Unprocessable | The URL returned a non-HTML response (PDF, JSON, image) or an HTTP error (404, 500). |
429 | Too Many Requests | Monthly conversion limit reached for your plan. Resets on the 1st of next month. |
502 | Bad Gateway | FetchMD could not reach the target URL — it may be down or blocking requests. |
504 | Gateway Timeout | The target URL took longer than 10 seconds to respond. |
500 | Server Error | Unexpected server error. Retry after a moment. Contact support if it persists. |
Example error response (429)
{
"error": "Monthly limit of 100 conversions reached. Upgrade at https://fetchmd.dev/pricing",
"plan": "free",
"limit": 100,
"used": 100
}Rate Limits
Limits are per API key and reset on the 1st of each month (free tier) or your billing date (paid plans). Failed requests (4xx/5xx) do not count toward your quota.
| Plan | Conversions/month | Price |
|---|---|---|
| Free | 100 | $0 |
| Starter | 2,000 | $9/mo |
| Pro | 10,000 | $29/mo |
| Scale | 50,000 | $99/mo |
| Enterprise | Unlimited | Custom |
When you hit your limit, the API returns 429 with your current plan, limit, and usage in the response body. The web converter at fetchmd.dev always works regardless of API quota. Upgrade your plan →
SDKs & Clients
FetchMD is a plain REST API — any HTTP client works. The MCP server is the recommended client for Claude Desktop and AI agent frameworks.
REST API
Any language. curl, fetch, requests, axios. No setup required.
@fetchmd/mcp-server
MCP server for Claude Desktop, Cursor, Windsurf, and any MCP-compatible client. 10 tools.
@fetchmd/client
TypeScript client with auto-retry, quota tracking, and submitAndWait() helper.
@fetchmd/langchain
LangChain document loader. Pass URLs → get Document[] objects for your RAG pipeline.
@fetchmd/llamaindex
LlamaIndex reader. loadData(urls) returns Document[] compatible objects.
OpenAPI spec
Machine-readable spec at /api/openapi for agent autodiscovery and code generation.
LangChain / LlamaIndex Integration
FetchMD works as a document loader for any RAG framework. Use it anywhere you need web or YouTube content as Document objects:
from langchain.docstore.document import Document
import requests
def fetch_as_documents(urls: list[str], api_key: str) -> list[Document]:
"""Fetch URLs via FetchMD and return LangChain Documents."""
docs = []
for url in urls:
res = requests.post(
"https://fetchmd.dev/api/convert",
headers={"Content-Type": "application/json", "X-API-Key": api_key},
json={"url": url},
)
data = res.json()
docs.append(Document(
page_content=data["markdown"],
metadata={"source": url, "title": data["title"], "tokens": data["tokenEstimate"]},
))
return docs
# Use with any LangChain chain
docs = fetch_as_documents(
["https://docs.stripe.com/api", "https://platform.openai.com/docs"],
api_key="fmd_your_key_here",
)MCP Server
The FetchMD MCP server gives Claude (and any MCP-compatible AI) the ability to read any webpage as clean Markdown — without any code. Just say “read this URL for me” and Claude handles the rest.
Tools exposed (10 total)
fetch_url_as_markdowntoolFetches any web URL and returns clean Markdown. Core tool — use for articles, docs, and any public webpage.
fetch_multiple_urlstoolFetches up to 5 URLs in parallel with metadata. Use for research and comparison tasks.
fetch_youtube_transcripttoolExtracts the full transcript from any YouTube video. Returns title, channel, and transcript as Markdown.
fetch_pdftoolDownloads and extracts text from any PDF URL. Works with research papers, reports, documentation.
fetch_github_repotoolConverts a GitHub repository to Markdown: metadata, file tree, README, and optionally source files.
fetch_sitemaptoolFetches a sitemap.xml and returns all URLs as a list. Supports sitemap index files.
crawl_linkstoolConverts a page to Markdown AND extracts all outbound links from it. Use to discover related pages.
submit_async_jobtoolSubmits a large batch of URLs for async processing. Returns a jobId immediately.
poll_jobtoolPolls the status of an async job. Returns results when complete.
check_quotatoolReturns current API quota usage: plan, conversions used, remaining, and reset date.
Claude Desktop config
{
"mcpServers": {
"fetchmd": {
"command": "npx",
"args": ["-y", "@fetchmd/mcp-server"],
"env": {
"FETCHMD_API_KEY": "fmd_your_key_here"
}
}
}
}Full installation guide with test prompts: fetchmd.dev/mcp →
Examples
RAG pipeline — ingest multiple docs
Fetch a list of documentation URLs, split into chunks, embed, and store in a vector database:
// RAG pipeline: ingest web content → chunk → embed → store
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
async function ingestUrls(urls: string[], apiKey: string) {
// 1. Fetch & convert to Markdown
const markdowns = await Promise.all(
urls.map(async (url) => {
const res = await fetch("https://fetchmd.dev/api/convert", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
body: JSON.stringify({ url }),
});
return res.json();
})
);
// 2. Split into chunks
const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
const allDocs = await Promise.all(
markdowns.map((m) =>
splitter.createDocuments([m.markdown], [{ source: m.url, title: m.title }])
)
);
// 3. Embed and store
const vectorStore = await MemoryVectorStore.fromDocuments(
allDocs.flat(),
new OpenAIEmbeddings()
);
return vectorStore;
}Handling errors gracefully
async function safeConvert(url: string, apiKey: string) {
const res = await fetch("https://fetchmd.dev/api/convert", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
body: JSON.stringify({ url }),
});
if (res.status === 429) {
const { plan, limit } = await res.json();
throw new Error(`Rate limited: ${plan} plan allows ${limit}/month. Upgrade at fetchmd.dev/pricing`);
}
if (res.status === 504) {
throw new Error(`Timeout: ${url} took longer than 10s to respond`);
}
if (!res.ok) {
const { error } = await res.json();
throw new Error(error);
}
return res.json();
}Claude prompt with converted content
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function summarizeUrl(url: string, fetchmdKey: string) {
// 1. Fetch content
const res = await fetch("https://fetchmd.dev/api/convert", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": fetchmdKey },
body: JSON.stringify({ url }),
});
const { markdown, title, tokenEstimate } = await res.json();
console.log(`Fetched "${title}" — ~${tokenEstimate} tokens`);
// 2. Pass to Claude
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [
{
role: "user",
content: `Summarize the following article in 3 bullet points:\n\n${markdown}`,
},
],
});
return message.content[0].type === "text" ? message.content[0].text : "";
}