Documentation

Overview

Link.md is a Markdown sharing service. Paste or drop a Markdown file, get a shareable link. Shares created without an account expire after 24 hours. Logged-in users can create permanent shares, set custom slugs, and upload attachments.

Shares support Markdown with extensions: math (LaTeX), syntax-highlighted code blocks, callouts, footnotes, wikilinks, and more. Public markdown and rendered HTML strip boundary YAML frontmatter/endmatter and accept CriticMarkup changes; stored files, version snapshots, editor buffers, and authenticated API file reads keep the original source.

Authentication

The API namespace is /api/v1. Most endpoints accept and return JSON; file download endpoints return raw file content with metadata in response headers.

Create an API key in Settings > API, or via the tokens API if you already have one. Include it in requests:

Authorization: Bearer YOUR_API_KEY

Unauthenticated requests are allowed for creating anonymous (24-hour) shares, but most operations require a key.

Create a share

Creating a share with files follows a three-step flow when any file body is omitted: create the share, upload missing files, then finalize. If all files include inline content, the share is published immediately and steps 2–3 are not needed.

Step 1: Create

curl -X POST https://link.md/api/v1/shares \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My Share",
    "slug": "my-share",
    "files": [
      {"filename": "index.md", "size": 13, "content_type": "text/markdown", "content": "# Hello\nWorld"},
      {"filename": "photo.png", "size": 102400, "content_type": "image/png"}
    ]
  }'

Optional fields: title, slug, visibility ("public", "unlisted", "private"; defaults to "public"), expires_in (seconds, minimum 60). Anonymous shares are capped at 24 hours; authenticated shares default to permanent and explicit expirations are capped at one year.

Text files can include content inline; for inline content, size must be the UTF-8 byte length of content. Binary files and any other files without inline content are uploaded in step 2. If all files have inline content, the share is auto-finalized and you can skip steps 2 and 3.

Allowed upload MIME types are image/png, image/jpeg, image/gif, image/webp, image/svg+xml, application/pdf, text/markdown, text/plain, and application/json. Generic or missing upload Content-Type is inferred only for .md/.markdown (text/markdown), .base (text/plain), and .canvas (application/json); other text/* MIME types are not accepted.

Returns 201:

{
  "id": "abc123...",
  "status": "draft",
  "files": [
    {"filename": "index.md", "upload_url": "/api/v1/shares/abc123.../upload/index.md", "uploaded": true},
    {"filename": "photo.png", "upload_url": "/api/v1/shares/abc123.../upload/photo.png", "uploaded": false}
  ],
  "edit_token": "TOKEN",
  "url": "https://link.md/USERNAME/my-share",
  "expires_at": null
}

Step 2: Upload

Upload each missing file body to its upload_url:

curl -X POST https://link.md/api/v1/shares/abc123.../upload/photo.png \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: image/png" \
  --data-binary @photo.png

Returns 200: {"ok": true, "size": 102400}

Step 3: Finalize

curl -X POST https://link.md/api/v1/shares/abc123.../finalize \
  -H "Authorization: Bearer YOUR_API_KEY"

Publishes the draft share and moves the public-read pointer to the finalized version snapshot.

Get a share

curl https://link.md/api/v1/shares/abc123... \
  -H "Authorization: Bearer YOUR_API_KEY"

Returns share metadata and a file list with download URLs.

Publish current edits

Live editor, collaborator, and agent edits do not automatically change what anonymous public readers see when a share has a published version pointer. Publish after saving to move the public-read pointer to the current canonical/collab state:

curl -X POST https://link.md/api/v1/shares/abc123.../publish \
  -H "Authorization: Bearer YOUR_API_KEY"

For edit-token access, send X-Edit-Token: TOKEN. The response includes the published version number:

{"ok": true, "version": 3}

Update share settings

curl -X PATCH https://link.md/api/v1/shares/abc123... \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "New Title", "slug": "new-slug"}'

Updatable fields: title, slug, visibility, expiration ("permanent" or "24h"). Owners and edit-token holders can update title, expiration, and non-private visibility; only owners can set or clear a slug or make a share private. If a live collaboration session is active, this endpoint returns 409 instead of overwriting in-flight collaborative edits.

Visibility values: "public", "unlisted", "private". Private shares are only accessible to the owner, edit-token holders, and collaborators.

Delete a share

curl -X DELETE https://link.md/api/v1/shares/abc123... \
  -H "Authorization: Bearer YOUR_API_KEY"

List your shares

curl https://link.md/api/v1/shares \
  -H "Authorization: Bearer YOUR_API_KEY"

Returns a paginated list of shares owned by the authenticated user. Use page and limit query parameters to page through results.

RSS feeds

Public notes for a user are available as an RSS feed:

curl https://link.md/USERNAME/rss.xml

Feeds include the 25 most recent public published notes. Item descriptions include a plain-text excerpt by default. To omit excerpts:

curl https://link.md/USERNAME/rss.xml?excerpt=false

Only public notes are included. Unlisted, private, draft, and expired notes are excluded.

File endpoints

Operate on individual files within a share by flat filename. The API route parameter is named :path, but it is a URL-encoded filename, not a directory path: nested paths with / or \\ are not supported. Useful for syncing tools like the Obsidian plugin.

List files

curl https://link.md/api/v1/shares/abc123.../files \
  -H "Authorization: Bearer YOUR_API_KEY"

Returns the entry file ID and file summaries:

{
  "entry_file_id": "file_123",
  "files": [
    {
      "id": "file_123",
      "filename": "index.md",
      "title": "index",
      "content_type": "text/markdown",
      "size_bytes": 1024,
      "is_entry": true,
      "sort_order": 0,
      "updated_at": "2026-06-04 13:25:12.474"
    }
  ]
}

Get a file

curl https://link.md/api/v1/shares/abc123.../files/index.md \
  -H "Authorization: Bearer YOUR_API_KEY"

Returns the raw file content with Content-Type set to the file's MIME type (not JSON-wrapped). Metadata is in response headers: X-Filename, X-Size, X-Is-Entry.

Create or update a file

curl -X PUT https://link.md/api/v1/shares/abc123.../files/notes.md \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: text/markdown" \
  --data-binary @notes.md

Creates a file if it doesn't exist, replaces it if it does. Send raw file content in the request body with an allowed Content-Type header. Use text/markdown for .md/.markdown, text/plain for .base, and application/json for .canvas; generic or missing upload types are inferred only for those filename extensions. File type and size limits apply; image, SVG, and PDF uploads are checked against their declared type, and SVGs are sanitized.

For anonymous shares, pass X-Edit-Token: TOKEN instead of Authorization.

Returns 200: {"ok": true, "filename": "notes.md", "size": 1024, "created": true}

Rename a markdown file

curl -X PATCH https://link.md/api/v1/shares/abc123.../files \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"old_filename":"Old.md","new_filename":"New.md","file_id":"file_123"}'

new_filename must end in .md. Returns 200: {"ok": true, "filename": "New.md", "file_id": "file_123", "title": "New"}.

Delete a file

curl -X DELETE https://link.md/api/v1/shares/abc123.../files/photo.png \
  -H "Authorization: Bearer YOUR_API_KEY"

Deletes a file from the share. The entry file (e.g. index.md) cannot be deleted. For anonymous shares, pass X-Edit-Token: TOKEN instead of Authorization.

Returns 200: {"ok": true}

API tokens

Manage API keys programmatically (requires an existing key).

Method Endpoint Description
POST /api/v1/tokens Create a new API key. Body: {"name": "...", "expires_in": 86400}
GET /api/v1/tokens List your API keys
DELETE /api/v1/tokens/:id Revoke an API key

Attachments

Attachments and sibling markdown files can be added to shares in two ways:

Supported file types

PNG, JPEG, GIF, WebP, SVG, PDF, Markdown (text/markdown), plain text (text/plain, including .base), and JSON (application/json, including .canvas). Other text/* MIME types are rejected.

SVGs are sanitized to remove scripts and other active content.

Multi-file shares

A share can contain multiple files. The entry file (typically index.md or the first markdown file) is rendered when you visit the share. Other files are accessible via relative links.

File URLs follow the pattern:

When a share has multiple files, a file list sidebar appears on the left to navigate between them. Anonymous public reads use the share's published version when one is set, so unpublished current edits do not leak through public entry or subfile URLs.

Collaboration

Real-time collaborative editing uses WebSocket connections authenticated via short-lived tokens.

Request a collaboration token

curl -X POST https://link.md/api/v1/shares/abc123.../collab-token \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"index.md"}'

The body must identify a markdown file using file_id, active_file_id, or, for non-browser clients, filename. Owners and collaborators can request tokens; edit-token users can request tokens when collaboration is enabled for the share. Returns a one-time token (valid for 30 seconds) and a WebSocket URL:

{
  "token": "TOKEN",
  "ws_url": "wss://link.md/collab/abc123...?token=TOKEN",
  "expires_in": 30
}

Connect to ws_url to join the collaborative editing session.

Invite collaborators

Owners can create an invite link for human collaborators:

curl -X POST https://link.md/api/v1/shares/abc123.../invite \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"role":"editor"}'

Roles are editor or viewer (editor by default). Change an active invite's role with PATCH /api/v1/shares/:id/invite and {"role":"viewer"} or {"role":"editor"}. Revoke the invite and remove collaborators with DELETE /api/v1/shares/:id/invite.

Agent collaborators

Invite an agent (or any HTTP client) to collaborate on a share without giving it your account-level API key. Each agent has a per-share Bearer token and a stable name that shows up in presence and edit attribution.

Agents are issued from the share's invite dropdown. Click Invite agent, pick a name, copy the snippet once. The token is bound to one share, can be revoked at any time, and is tied to your user account (deleting your account or the share revokes it).

Issue an agent token

curl -X POST https://link.md/api/v1/shares/abc123.../agents \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_name": "claude", "role": "editor"}'

Returns the token exactly once:

{
  "agent_name": "claude",
  "role": "editor",
  "token": "AGENT_TOKEN",
  "prefix": "AGENT_TO",
  "created_at": "2026-05-15T14:30:00Z"
}

agent_name must match [a-z0-9][a-z0-9_-]{0,31} and is unique per share. role is editor (default) or viewer.

List and revoke

curl https://link.md/api/v1/shares/abc123.../agents \
  -H "Authorization: Bearer YOUR_API_KEY"

curl -X DELETE https://link.md/api/v1/shares/abc123.../agents/claude \
  -H "Authorization: Bearer YOUR_API_KEY"

Agent endpoints

All authenticated with the agent's Bearer token. The agent token is bound to a single share. Calls against any other share return 403.

Read state

curl https://link.md/api/v1/shares/abc123.../state \
  -H "Authorization: Bearer AGENT_TOKEN" \
  -H "X-Agent-Id: claude"

Returns the current doc and all live cursors (humans and agents):

{
  "version": 13,
  "content": "# Hello",
  "title": "Hello",
  "awareness": [["claude", {"agent":{"name":"claude"}, "cursor":{"anchor":4,"head":4}}]]
}

Push an edit

Agents send the full new doc plus the version they based it on. The server applies the change, bumps the version, and broadcasts it to all live editors. If version is stale you get 409 with the current state.

curl -X POST https://link.md/api/v1/shares/abc123.../ops \
  -H "Authorization: Bearer AGENT_TOKEN" \
  -H "X-Agent-Id: claude" \
  -H "Content-Type: application/json" \
  -d '{"version": 13, "content": "# Hello\n\nNew line.", "title": "Hello"}'

Response: {"version": 14} on success, or 409 with { "error": "conflict", "version": ..., "content": ..., "title": ... } on version mismatch.

Only editor-role agents can push. viewer agents get 403.

Heartbeat presence

curl -X POST https://link.md/api/v1/shares/abc123.../presence \
  -H "Authorization: Bearer AGENT_TOKEN" \
  -H "X-Agent-Id: claude" \
  -H "Content-Type: application/json" \
  -d '{"cursor": {"anchor": 10, "head": 14}}'

Cursor is optional. Send {} as a liveness ping. Presence times out after 90 seconds without a heartbeat, so agents that want to stay visible should ping every ~60 seconds. cursor: null explicitly clears the agent's cursor while keeping it in the presence list.

Notes

Markdown public output

Public markdown responses (.md, Accept: text/markdown, and /raw) and rendered HTML use the same published-output normalization:

Authenticated API file reads return the original source.

Limits

Limit Anonymous Authenticated
Share expiration 24 hours Permanent or custom
Max file size 2 MB 10 MB
Images per share 4 20
Additional file creation cap current image limit + 10 files current image limit + 10 files
Total size per share 5 MB 50 MB
Storage quota 50 MB (default)
Browser editor markdown body 1 MB 1 MB

Rate limiting is D1-backed. Browser share creation, Obsidian SSO exchange, API share creation, API file mutations, and collab-token minting can return 429 with retry details.

Error codes

Status Meaning
400 Validation failed (empty content, invalid slug, invalid visibility, bad filename, etc.)
401 Missing or invalid API key / edit token
403 Forbidden (not the owner/editor, CSRF check failed, or cross-share/viewer agent action)
404 Share not found, or private/draft share hidden from unauthorized callers
409 Slug or ID collision, active collab conflict guard, or agent edit version conflict
410 Share has expired
413 File or share exceeds size limit / storage quota exceeded
429 Rate limited — check retry details in the response
Drop .md file