# 2md.chat

> Turns a PUBLIC share link of an AI conversation, or a public Claude artifact, into Markdown.
> No key, no login, no rate card. Every answer is UTF-8 and may be fetched from any origin (CORS *).

The one thing to remember: put `https://2md.chat/` in front of the share link and fetch it.

    GET https://2md.chat/https://chatgpt.com/share/<id>      ->  200 text/markdown

## Supported links

- ChatGPT: https://chatgpt.com/share/<id>
- Claude: https://claude.ai/share/<id>
- Claude artifact: https://claude.ai/artifact/<id>  or  https://claude.ai/public/artifacts/<uuid>
- Gemini: https://gemini.google.com/share/<id>  or  https://g.co/gemini/share/<id>
- Grok: https://grok.com/share/<id>
- DeepSeek: https://chat.deepseek.com/share/<id>
- Kimi: https://www.kimi.com/share/<id>
- Qwen: https://chat.qwen.ai/s/<id>
- GLM (Z.ai): https://chat.z.ai/s/<id>
- Mistral Le Chat: https://chat.mistral.ai/chat/<id>
- Perplexity: https://www.perplexity.ai/search/<slug>
- Meta AI: https://www.meta.ai/share/c/<id>

Only public links work. A private or deleted link answers with an error, never with partial content.

## Routes

GET /<share-link>          e.g. GET /https://chatgpt.com/share/<id>
    The Markdown itself (text/markdown). Nothing else - no HTML, no interface.
    Send "Accept: application/json" to get the JSON result instead (see /api/convert).
    Flags in the query string (any other query parameter belongs to the share link):
      ?frontmatter=1   YAML frontmatter (title, source, provider, exported) instead of the header lines
      ?bare=1          content only, no title or header
    Errors are text/plain, "Error: <message> (<code>)", with a 4xx status.

GET /api/convert?url=<share-url>[&frontmatter=1|&bare=1]
POST /api/convert      body: {"url": "<share-url>", "frontmatter": true | "bare": true}
    200: {"ok": true, "provider", "title", "sourceUrl", "markdown", "filename", "messageCount"}
    4xx: {"ok": false, "error": "<message>", "code": "<code>"}
    OpenAPI: /openapi.json

POST /mcp
    MCP server (Streamable HTTP, stateless, JSON answers). One tool: convert_share_link {url, header?}.
    Claude Code:  claude mcp add --transport http 2md https://2md.chat/mcp
    Any client:   {"mcpServers": {"2md": {"type": "http", "url": "https://2md.chat/mcp"}}}

POST /api/share        body: {"markdown", "filename", "title", "sourceUrl", "maxViews"?, "expiresInSeconds"?}
    Stores Markdown and returns {"url": "/md/<id>", "rawUrl": "/md/<id>/raw", "downloadUrl": "/md/<id>/download"}.
    Links expire after 24 hours by default.

GET /md/<id>           the stored Markdown for any client that does not ask for text/html; browsers get the interface
GET /md/<id>/raw       always the Markdown
GET /md/<id>/download  the same, as an attachment
GET /                  this file for agents and curl; the web interface for browsers; /?url=<share-url> pre-fills it

## Output shape

    # <title>

    Source: <share-url>
    Provider: <name>
    Exported: <ISO time>

    ---

    ## User

    <message>

    ## Assistant

    <message, Markdown kept as written: code fences, tables, links>

Thinking/reasoning blocks are left out. Artifacts come out as one document: scripts are run and
"show more" is expanded, so long tables are complete.

## Error codes

unsupported_provider   the link is not one of the sources above
invalid_url            not a URL
*_not_public           the share or artifact is private
*_not_found            deleted or never existed
*_http_error           the source answered with an error (shown as 424)
*_cloudflare_challenge the source asked for a browser check this server could not pass

## Example

curl https://2md.chat/https://claude.ai/artifact/<id>?frontmatter=1
