Documentation·v1.0

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.

1

Get a free API key

fetchmd.dev/api-key — enter your email, copy the key. Takes 10 seconds.

2

Make your first request

shell
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:

json
{
  "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.

Authenticated 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"}'

You can also pass the key as a Bearer token:

Bearer token (alternative)
-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.

shell
curl -X POST https://fetchmd.dev/api/keys \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com"}'
response
{
  "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.

POSThttps://fetchmd.dev/api/convert

Request body

urlstringrequired

The URL to fetch and convert. Must include protocol (https:// or http://). The page must return text/html content.

Response body

markdownstring

The full page content converted to clean Markdown. Headings, links, code blocks, tables, and lists are preserved. Navigation, ads, scripts, and boilerplate are stripped.

urlstring

The final URL after redirects are followed.

titlestring

The page title extracted from the <title> tag.

tokenEstimatenumber

Estimated token count for the markdown output (characters / 4). Useful for context window planning.

fetchTimeMsnumber

Time in milliseconds to fetch the page. Does not include conversion time.

htmlSizenumber

Original HTML size in bytes.

markdownSizenumber

Markdown output size in bytes.

compressionRationumber

Percentage reduction in size from HTML to Markdown. Typically 80–95% for content-heavy pages.

Limits

  • · Fetch timeout: 10 seconds per request
  • · Only http:// and https:// protocols
  • · Target must return text/html content-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.

POSThttps://fetchmd.dev/api/keys

Request body

emailstringrequired

Your email address. Used to look up your existing key if you call this endpoint again. We don't send marketing emails.

Response body

keystring

Your API key. Format: fmd_ followed by 48 hex characters. Store this securely — it won't be shown again via API.

existingboolean

true 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.

POSThttps://fetchmd.dev/api/youtube

Request body

urlstringrequired

YouTube video URL. Supports: youtube.com/watch?v=, youtu.be/, youtube.com/shorts/, youtube.com/embed/. Or pass an 11-character video ID directly.

timestampsboolean

Include timestamps in the transcript. Default: true. Set to false for clean paragraph output (better for LLMs).

Response

markdownstring

Full transcript as Markdown: title, channel, description, and timestamped or paragraph transcript.

videoIdstring

The 11-character YouTube video ID.

titlestring

Video title.

channelNamestring

Channel name.

languagestring

Language code of the selected transcript track (e.g. en).

segmentCountnumber

Number of caption segments parsed.

durationSecondsnumber

Video duration in seconds.

tokenEstimatenumber

Estimated token count for the markdown output.

shell
curl -X POST https://fetchmd.dev/api/youtube \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","timestamps":false}'
response
{
  "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).

POSThttps://fetchmd.dev/api/pdf

Request body

urlstringrequired

Direct link to a PDF file. Must return application/pdf content-type (or include .pdf in the path). Maximum 10MB.

Response

markdownstring

Extracted text as Markdown with document title, author, and page count header.

titlestring

PDF title from document metadata, or filename if not set.

authorstring

Author from PDF metadata (may be empty).

pagesnumber

Detected page count.

pdfSizenumber

Original PDF size in bytes.

markdownSizenumber

Markdown output size in bytes.

compressionRationumber

Percentage size reduction. Typically 80–95% for text-heavy PDFs.

tokenEstimatenumber

Estimated token count for the markdown output.

shell
curl -X POST https://fetchmd.dev/api/pdf \
  -H "Content-Type: application/json" \
  -d '{"url":"https://arxiv.org/pdf/2303.08774"}'
response
{
  "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.

POSThttps://fetchmd.dev/api/github

Request body

urlstringrequired

GitHub URL. Supported formats: github.com/owner/repo, github.com/owner/repo/tree/branch, github.com/owner/repo/blob/branch/path/to/file.

includeCodeboolean

Fetch and include source file contents in the output. Default: false. When true, fetches text files up to 50KB each, up to maxFiles files.

maxFilesnumber

Max number of source files to include when includeCode is true. Default: 20, max: 50.

Response

markdownstring

Repo overview as Markdown: header, file tree, README, and optionally source files.

ownerstring

Repository owner login.

repostring

Repository name.

descriptionstring

Repository description.

starsnumber

Star count.

languagestring

Primary programming language.

defaultBranchstring

Default branch name.

fileCountnumber

Total number of files in the repository.

tokenEstimatenumber

Estimated token count.

shell
curl -X POST https://fetchmd.dev/api/github \
  -H "Content-Type: application/json" \
  -d '{"url":"https://github.com/vercel/next.js","includeCode":false}'
response
{
  "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.

POSThttps://fetchmd.dev/api/bulk

Request body

urlsstring[]required

Array 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

resultsarray

Array of result objects in input order. Successful: url, markdown, title, tokenEstimate, compressionRatio. Failed: adds error string.

shell
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.

POSThttps://fetchmd.dev/api/jobs→ 202 Accepted

Request body

urlsstring[]required

Array of URLs to convert. Maximum 200 per job. Requires API key. Quota is reserved upfront.

Submit — returns 202

shell
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"]}'
response 202
{
  "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: processingcomplete (or failed). Poll every 1–2 seconds.

shell
curl https://fetchmd.dev/api/jobs/job_01jt5k2n8r4p \
  -H "X-API-Key: fmd_your_key_here"
response (complete)
{
  "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.

GEThttps://fetchmd.dev/api/quota
shell
curl https://fetchmd.dev/api/quota \
  -H "X-API-Key: fmd_your_key_here"
response
{
  "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.

CodeNameDescription
400Bad RequestMissing or invalid url field, or invalid email for key generation.
401UnauthorizedInvalid or unrecognized API key. Get one at fetchmd.dev/api-key.
422UnprocessableThe URL returned a non-HTML response (PDF, JSON, image) or an HTTP error (404, 500).
429Too Many RequestsMonthly conversion limit reached for your plan. Resets on the 1st of next month.
502Bad GatewayFetchMD could not reach the target URL — it may be down or blocking requests.
504Gateway TimeoutThe target URL took longer than 10 seconds to respond.
500Server ErrorUnexpected server error. Retry after a moment. Contact support if it persists.

Example error response (429)

json
{
  "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.

PlanConversions/monthPrice
Free100$0
Starter2,000$9/mo
Pro10,000$29/mo
Scale50,000$99/mo
EnterpriseUnlimitedCustom

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:

python — langchain
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_markdowntool

Fetches any web URL and returns clean Markdown. Core tool — use for articles, docs, and any public webpage.

fetch_multiple_urlstool

Fetches up to 5 URLs in parallel with metadata. Use for research and comparison tasks.

fetch_youtube_transcripttool

Extracts the full transcript from any YouTube video. Returns title, channel, and transcript as Markdown.

fetch_pdftool

Downloads and extracts text from any PDF URL. Works with research papers, reports, documentation.

fetch_github_repotool

Converts a GitHub repository to Markdown: metadata, file tree, README, and optionally source files.

fetch_sitemaptool

Fetches a sitemap.xml and returns all URLs as a list. Supports sitemap index files.

crawl_linkstool

Converts a page to Markdown AND extracts all outbound links from it. Use to discover related pages.

submit_async_jobtool

Submits a large batch of URLs for async processing. Returns a jobId immediately.

poll_jobtool

Polls the status of an async job. Returns results when complete.

check_quotatool

Returns current API quota usage: plan, conversions used, remaining, and reset date.

Claude Desktop config

~/Library/Application Support/Claude/claude_desktop_config.json
{
  "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:

typescript — langchain + openai
// 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

typescript
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

typescript — anthropic sdk
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 : "";
}