# fmrl API v1

Publish a page from any agent or script and get a link. No account: mint a key, publish, share the URL. Pages last seven days unless someone keeps them from the page itself. Base URL: `https://fmrl.site/api/v1`.

## Install the plugin instead

Claude Code:

```
/plugin marketplace add toogreatwtf/fmrl-plugin
/plugin install fmrl@fmrl-plugin
```

Any MCP client (Claude Desktop, Cursor, Codex, Windsurf, and the rest):

```json
{ "mcpServers": { "fmrl": { "command": "npx", "args": ["-y", "fmrl-mcp"] } } }
```

No key step. The server mints a key the first time it publishes and keeps it in your config directory (`~/.config/fmrl/credentials.json`, or `%APPDATA%\fmrl\credentials.json` on Windows). `FMRL_API_KEY` overrides it; `FMRL_API_URL` points the server at another host (default `https://fmrl.site`).

## curl

```
curl -sX POST https://fmrl.site/api/v1/keys
curl -sX POST https://fmrl.site/api/v1/publish \
  -H "Authorization: Bearer fmrl_…" -H "Content-Type: application/json" \
  -d '{"format":"md","content":"# hello"}'
```

## Conventions

JSON in and out. Every route except key minting takes `Authorization: Bearer fmrl_…`. Errors are `{"error": {"code": "…", "message": "…"}}` with the HTTP status.

| Status | `code` | When |
|---|---|---|
| 400 | `bad_request` | malformed JSON, missing content, a format other than `html` or `md`, a label over 64 characters, or `encrypted` with format `md` |
| 401 | `invalid_key` | missing, malformed, or unknown key |
| 402 | `quota_exhausted` | the key's free publishes for the month are used; `error.resets_at` says when it resets |
| 404 | `not_found` | no such page, an expired or removed page, or a page this key does not own (on delete) |
| 413 | `too_large` | content over 2 MiB |
| 422 | `rejected` | the content failed the same checks the paste form runs; the message names the top signal. A body sent with `encrypted: true` that isn't a valid MARKYENC v2 envelope also answers `rejected` here, naming no signal |
| 429 | `rate_limited` | too many keys from one network, or too many publishes from one address; `Retry-After` is set |

## Routes

### POST /api/v1/keys

Body optional: `{"label": "…"}` (64 characters at most). No authentication. Limited to 5 keys a day per network.

```
201 {"key": "fmrl_…", "prefix": "fmrl_ab12", "created_at": "…", "quota": {"publishes": 25, "period": "month"}}
```

The key is shown once. Keep it; only its hash is stored.

### POST /api/v1/publish

Body: `{"format": "html" | "md", "content": "…", "title": "…", "encrypted": false}`. `format` may be left out and is detected from the content. `title` overrides the one taken from the content.

```
201 {"id": "…", "url": "https://fmrl.site/{id}", "raw_url": "https://fmrl.site/{id}/raw",
     "manage_url": "https://fmrl.site/manage/{id}#k=…", "expires_at": "…", "status": "live" | "reviewing"}
```

`url` is the page. `manage_url` removes it from any browser; hand it to a person if they should be able to. `raw_url` is informational: the raw route answers only our viewer, so keep your own copy of the bytes. A `reviewing` status means a content check is still finishing; the page shows a short interstitial until it does. Publishing runs the paste form's checks with the human challenge skipped. Only an accepted publish counts against the quota.

**Private pages.** Send `"encrypted": true` with `content` set to a MARKYENC v2 envelope you sealed yourself: the text `MARKYENC` followed by `{"v":2,"alg":"aes-256-gcm","kdf":"none"|"pbkdf2","salt":"…","nonce":"…","data":"…"}`, AES-256-GCM with a 12-byte nonce, the 16-byte tag appended to `data`, fields in standard base64. With `kdf` `none` the key is 32 random bytes; hand readers `url` plus `#p=<key as 43 base64url characters>` — the server never sees the key and does not add the fragment. With `kdf` `pbkdf2` the key is PBKDF2-HMAC-SHA256 over a passphrase, 600,000 iterations, 16-byte `salt`, and readers type the passphrase into the page. The plaintext must be a complete HTML document (render Markdown first); `format` must be `html` or left out, and `title` is ignored. The 2 MiB cap applies to the envelope, so about 1.4 MB of HTML fits. A private page has no title or preview, is not scanned, and is removed on a credible report without review. There is no key recovery.

### GET /api/v1/docs/{id}

Any valid key may read any page the viewer would show.

```
200 {"id": "…", "url": "…", "status": "live" | "reviewing" | "pinned", "format": "html" | "md", "size": 1234,
     "expires_at": "…" | null, "pinned": false, "cid": "bafy…"}
```

`expires_at` is `null` and `pinned` is `true` once someone has kept the page; `cid` is present only then.

### DELETE /api/v1/docs/{id}

Owner key only: the key that published the page. `204` on success. Same effect as the manage link: the page answers 410 from now on, the bytes are deleted, and a kept page is unpinned if nothing else holds its CID. No refund.

### GET /api/v1/me

```
200 {"prefix": "fmrl_ab12", "created_at": "…", "quota": {"publishes": {"used": 3, "limit": 25, "resets_at": "…"}}}
```

## Limits

25 publishes a month per key, resetting on the first of the month (UTC). 5 keys a day per network. 2 MiB per page. Pages expire after seven days unless kept from the page itself. Content is subject to the [acceptable use policy](https://fmrl.site/aup); anything the checks reject, or a report takes down, stays down.

Keeping a page, stashing bytes, and metered plans are not on the API yet.
