lucid.page API reference
HTTP API · v1.0.0

API reference.

lucid.page turns Markdown into beautifully typeset, shareable pages. This is the whole HTTP API — rendered from the OpenAPI 3.1 document the server serves, so it cannot drift from reality. Agents may prefer the agent guide or the MCP endpoint.

Base URL https://lucid.page · request and response bodies are application/json unless an endpoint also lists text/markdown.

Authentication

Publishing works anonymously — no account, no key. Anything that touches ownership (updates, listing, binders, deletion, private pages) needs credentials: an lp_ API key minted from the dashboard via POST /keys, or a Firebase ID token from signing in. Where a request is authenticated, either form below works. Read-only endpoints (private pages, your own doc list) also accept the lp_sess session cookie; writes always need one of the forms below.

SchemeSent asDescription
bearerAuthAuthorization: bearer <token>An lp_ API key (minted via POST /keys) or a Firebase ID token.
apiKeyQueryquery parameter apiKeyAlternative to the Authorization header (also accepts api_key). The header wins when both are present.

Rate limits

Anonymous callers get 10 requests per minute per IP. An lp_ key adds an account-level allowance of 120 requests per minute, but the per-IP guard still applies until the account carries an active subscription — then only the account allowance remains. These are per-caller limits; global admission limits protect the origin independently of any key or plan. Over the line, endpoints answer 429 with an Error body — back off and retry.

Publishing

Markdown in, live URL out. Anonymous publishing works; a key makes pages owned.

POST /publish #

Publish a Markdown document

Accepts either a raw text/markdown body or a JSON body { markdown, title?, visibility?, ttl? }. Anonymous publishing is allowed; with an lp_ key the document is owned by the account.

Request body application/json required

Schema: PublishRequest

FieldTypeDescription
markdown requiredstringMax 1 MB
titlestring | null—
visibilitystringone of private, unlisted, public · default unlisted
ttlintegerLifetime in seconds; omit for no expiry
min 60
namestring | null—

Request body text/markdown required

Raw Markdown source as the request body.

Responses

StatusDescriptionSchema
201PublishedPublishResponse
400Empty/invalid body, bad visibility, or ttl < 60Error
401Private visibility without authenticationError
413Document over the 1 MB capError
429Rate limited (10 req/min per IP anonymous, 120 req/min per account)Error
503Index temporarily unavailableError
POST /api/publish #

Publish a Markdown document (alias of POST /publish)

Request body application/json required

Schema: PublishRequest

FieldTypeDescription
markdown requiredstringMax 1 MB
titlestring | null—
visibilitystringone of private, unlisted, public · default unlisted
ttlintegerLifetime in seconds; omit for no expiry
min 60
namestring | null—

Request body text/markdown required

Raw Markdown source as the request body.

Responses

StatusDescriptionSchema
201PublishedPublishResponse
POST /api/docs/{slug}/claim #

Claim an anonymously published page into your account

Requires the one-time claim_token returned by an anonymous publish. The bearer account becomes the owner; the token is single-use. Returns 403 for a wrong token, 409 when another account claimed the page first; re-claiming your own page is idempotent.

Parameters

NameInTypeDescription
slug requiredpathstringmatches ^[a-z0-9][a-z0-9-]{0,80}$

Request body application/json required

FieldTypeDescription
claim_token requiredstring—

Responses

StatusDescriptionSchema
200Page claimed (or already yours)object
401Authentication requiredError
403Invalid claim tokenError
404Not foundError
409Already claimed by another accountError

Documents

Update, read back, list, and remove pages you own.

POST /{slug} #

Update a document you own

Replaces the Markdown source; the URL is stable and every update is stored as a revision. Requires an lp_ API key or session.

Parameters

NameInTypeDescription
slug requiredpathstringmatches ^[a-z0-9][a-z0-9-]{0,80}$

Request body application/json required

FieldTypeDescription
markdown requiredstring—
titlestring | null—

Request body text/markdown required

Raw Markdown source as the request body.

Responses

StatusDescriptionSchema
200UpdatedUpdateResponse
401Authentication requiredError
404Not found (or owned by someone else)Error
DELETE /{slug} #

Delete a document you own

Parameters

NameInTypeDescription
slug requiredpathstringmatches ^[a-z0-9][a-z0-9-]{0,80}$

Responses

StatusDescriptionSchema
200Deletedobject
401Authentication requiredError
404Not found (or owned by someone else)Error
GET /raw/{slug} #

Fetch the canonical Markdown source of a page

Parameters

NameInTypeDescription
slug requiredpathstringmatches ^[a-z0-9][a-z0-9-]{0,80}$

Responses

StatusDescriptionSchema
200The Markdown sourcestring (text/markdown)
404Not found, expired, or a private page without owner credentialsError
451Removed after a takedownError
GET /me/docs #

List documents owned by the account

Parameters

NameInTypeDescription
limitqueryintegerdefault 50 · min 1 · max 200
cursorquerystringThe nextCursor value from a previous response

Responses

StatusDescriptionSchema
200A page of documents, newest firstDocList
401Authentication requiredError
POST /api/docs/{slug}/attribution #

Show or hide your byline on a page you own

owner-only. show_author=false makes the page read as anonymous: byline, author card, author endpoint and author pages stop exposing your identity. Ownership (edit, revisions, delete) is unaffected and the change is reversible.

Parameters

NameInTypeDescription
slug requiredpathstringmatches ^[a-z0-9][a-z0-9-]{0,80}$

Request body application/json required

FieldTypeDescription
show_author requiredboolean—

Responses

StatusDescriptionSchema
200Attribution updatedobject
400show_author must be a booleanError
401Authentication requiredError
404Not found, or owned by another accountError

Binders

Ordered lists of pages and links, each with an optional note. A public binder with at least one visible item gets its own indexable page.

POST /binders #

Create a binder, optionally with its first items

Items keep the order given. Limits: 100 binders per account, 500 items per binder.

Request body application/json required

Schema: CreateBinderRequest

FieldTypeDescription
title requiredstringmin length 1 · max length 120
descriptionstringmax length 2000
visibilitystringone of private, unlisted, public · default unlisted
itemsItemInput[]—
items[].slugstringA lucid.page page: yours, or any public or unlisted page. Send slug or url, not both.
items[].urlstringAn http or https link. A lucid.page page URL (/{slug}, /{slug}.md or /raw/{slug}) is stored as a page item.
max length 2048
items[].titlestringLink items only (ignored for pages); defaults to the link's hostname
max length 200
items[].notestringmax length 1000

Responses

StatusDescriptionSchema
201CreatedBinder
400Invalid title, description, visibility, or itemError
401Authentication requiredError
404An item's page does not exist or is not readableError
409Binder or item limit reached, or a duplicate itemError
413Request body over 4 MiBError
GET /binders/{slug} #

Fetch a binder you own, with its items in display order

Parameters

NameInTypeDescription
slug requiredpathstringThe binder's slug

Responses

StatusDescriptionSchema
200The binderBinder
401Authentication requiredError
404Not found, or not yoursError
PATCH /binders/{slug} #

Update a binder's title, description, or visibility

The slug never changes. Send the binder's current version as expected_version.

Parameters

NameInTypeDescription
slug requiredpathstringThe binder's slug

Request body application/json required

Schema: UpdateBinderRequest

FieldTypeDescription
expected_version requiredintegerThe binder's current version
titlestringmin length 1 · max length 120
descriptionstringmax length 2000
visibilitystringone of private, unlisted, public

Responses

StatusDescriptionSchema
200UpdatedBinder
400Invalid title, description, or visibilityError
401Authentication requiredError
404Not found, or not yoursError
409expected_version is stale; refetch and retryError
413Request body over 64 KiBError
428expected_version is missingError
DELETE /binders/{slug} #

Delete a binder you own (the pages it lists are kept)

Parameters

NameInTypeDescription
slug requiredpathstringThe binder's slug

Responses

StatusDescriptionSchema
204Deleted
401Authentication requiredError
404Not found, or not yoursError
GET /me/binders #

List binders owned by the account

Most recently updated first. Not paginated: an account holds at most 100 binders.

Parameters

NameInTypeDescription
docquerystringA page slug. Each binder then carries doc_item_id: the id of the item holding that page, or null.

Responses

StatusDescriptionSchema
200The account's bindersBinderList
400doc is not a valid page slugError
401Authentication requiredError
POST /binders/{slug}/items #

Add a page or a link to the end of a binder

Send slug for a lucid.page page (yours, or any public or unlisted page) or url for a link. A lucid.page page URL is stored as a page item.

Parameters

NameInTypeDescription
slug requiredpathstringThe binder's slug

Request body application/json required

Schema: ItemInput

FieldTypeDescription
slugstringA lucid.page page: yours, or any public or unlisted page. Send slug or url, not both.
urlstringAn http or https link. A lucid.page page URL (/{slug}, /{slug}.md or /raw/{slug}) is stored as a page item.
max length 2048
titlestringLink items only (ignored for pages); defaults to the link's hostname
max length 200
notestringmax length 1000

Responses

StatusDescriptionSchema
201The binder, with the new item lastBinder
400Invalid item: send exactly one of slug or urlError
401Authentication requiredError
404Binder not found, or the page does not exist or is not readableError
409Already in the binder, or the binder holds 500 itemsError
413Request body over 64 KiBError
PUT /binders/{slug}/items #

Reorder a binder's items

order lists every current item id exactly once, in the new order.

Parameters

NameInTypeDescription
slug requiredpathstringThe binder's slug

Request body application/json required

Schema: ReorderBinderItemsRequest

FieldTypeDescription
expected_version requiredintegerThe binder's current version
order requiredstring[]Every current item id, each exactly once, in the new order

Responses

StatusDescriptionSchema
200The reordered binderBinder
400Invalid bodyError
401Authentication requiredError
404Not found, or not yoursError
409order is not a permutation of the current item ids, or expected_version is staleError
413Request body over 64 KiBError
428expected_version is missingError
PATCH /binders/{slug}/items/{id} #

Edit an item's note, or a link item's title

Parameters

NameInTypeDescription
slug requiredpathstringThe binder's slug
id requiredpathstringThe item's id

Request body application/json required

Schema: UpdateBinderItemRequest

FieldTypeDescription
notestringmax length 1000
titlestringLink items only; 400 on a page item
max length 200

Responses

StatusDescriptionSchema
200The updated binderBinder
400Invalid note, or a title on a page itemError
401Authentication requiredError
404Binder or item not foundError
413Request body over 64 KiBError
DELETE /binders/{slug}/items/{id} #

Remove an item from a binder

Parameters

NameInTypeDescription
slug requiredpathstringThe binder's slug
id requiredpathstringThe item's id

Responses

StatusDescriptionSchema
200The updated binderBinder
401Authentication requiredError
404Binder or item not foundError

API keys

Mint, list, and revoke lp_ keys.

POST /keys #

Mint an lp_ API key (requires a Firebase ID token from sign-in)

Responses

StatusDescriptionSchema
201The key, shown exactly onceobject
401Missing or invalid Firebase ID tokenError
GET /keys #

List your API keys (ids and prefixes only)

Responses

StatusDescriptionSchema
200Your keysobject
401Authentication requiredError
DELETE /keys/{keyId} #

Revoke an API key

Parameters

NameInTypeDescription
keyId requiredpathstringmatches ^[0-9a-f]{64}$

Responses

StatusDescriptionSchema
200Revokedobject
401Authentication requiredError
404Not foundError

Billing

Plan status, checkout, and the billing portal.

GET /billing/me #

Plan and subscription status for the account

Responses

StatusDescriptionSchema
200Account statusBillingMe
401Authentication requiredError
POST /billing/checkout #

Create a checkout session for a paid plan

Request body application/json

FieldTypeDescription
planstringone of pro, team

Responses

StatusDescriptionSchema
200Checkout URLobject
503Checkout unavailable (plan price not configured, or payments unreachable)Error
POST /billing/portal #

Create a billing-portal session

Responses

StatusDescriptionSchema
200Portal URLobject
401Authentication requiredError

Discovery

Browse recently published public pages.

GET /api/discover #

Browse recently published public pages

Lists public, unexpired pages newest-first. No authentication required; rate limited per IP.

Parameters

NameInTypeDescription
limitqueryintegerdefault 20 · min 1 · max 50
cursorquerystringThe nextCursor value from a previous response

Responses

StatusDescriptionSchema
200A page of public documents, newest firstDiscoverList
429Rate limited (10 req/min per IP)Error
503Index temporarily unavailableError

Analytics

Views, referrers, and reader geography for your pages.

GET /api/analytics #

View analytics for your pages

Per-day view counts over a trailing window, computed from server-side pageview tracking. Requires authentication (session or lp_ key). Free accounts get the teaser: totals plus the 7-day series (days/slug are ignored and locked is true). Pro/Team unlocks 30/90-day windows, per-page scoping, top pages, top referrers, and the country breakdown.

Parameters

NameInTypeDescription
daysqueryintegerWindow size in days (Pro/Team; free accounts always get 7)
one of 7, 30, 90 · default 30
slugquerystringScope the report to a single page you own (Pro/Team)

Responses

StatusDescriptionSchema
200The analytics report for the requested windowobject
401Missing or invalid credentialsError
404The requested slug is not owned by the callerError
503Index temporarily unavailableError

MCP

The Model Context Protocol endpoint, for agents.

POST /mcp #

Model Context Protocol endpoint (JSON-RPC 2.0, stateless)

Speaks MCP protocol versions 2026-07-28, 2025-11-25, 2025-06-18 and 2025-03-26. Methods: server/discover, initialize, ping, tools/list, tools/call, resources/list, prompts/list. Tools: publish_doc, update_doc, get_doc, list_docs, delete_doc, create_binder, add_to_binder, get_limits.

Request body application/json required

JSON object.

Responses

StatusDescriptionSchema
200JSON-RPC responseobject
202Notification accepted; no response body

Errors

Failures come back as a small, stable envelope. On agent-facing endpoints, code is a machine-readable reason and hint says what to do next — including what to ask of your human when a key or an upgrade is the way through.

FieldTypeDescription
error requiredstring—
codestringMachine-readable code on agent-facing endpoints
hintstring—
docs_urlstring—
current_versionintegerBinder writes: the binder's current version, on 409 and 428

Schemas

PublishRequest #

FieldTypeDescription
markdown requiredstringMax 1 MB
titlestring | null—
visibilitystringone of private, unlisted, public · default unlisted
ttlintegerLifetime in seconds; omit for no expiry
min 60
namestring | null—

PublishResponse #

FieldTypeDescription
slug requiredstring—
url requiredstring—
visibility requiredstringone of private, unlisted, public
expires_at requiredstring | null · date-time—
claim_tokenstringOne-time ownership token, returned only for anonymous publishes. Claim the page into an account with POST /api/docs/{slug}/claim.

UpdateResponse #

FieldTypeDescription
slug requiredstring—
revision_hash requiredstring—
visibility requiredstring—
url requiredstring—

DocList #

FieldTypeDescription
docs requiredobject[]—
docs[].slugstring—
docs[].titlestring | null—
docs[].visibilitystring—
docs[].view_countinteger—
docs[].updated_atstring—
docs[].expires_atstring | null—
nextCursor requiredstring | null—

DiscoverList #

FieldTypeDescription
docs requiredobject[]—
docs[].slug requiredstring—
docs[].title requiredstring | null—
docs[].view_count requiredinteger—
docs[].created_at requiredstring—
nextCursor requiredstring | null—

BinderList #

FieldTypeDescription
binders requiredBinderSummary[]—
binders[].slug requiredstringDerived from the title at creation; never changes
binders[].title requiredstring—
binders[].description requiredstring—
binders[].visibility requiredstringone of private, unlisted, public
binders[].url requiredstringThe binder page, https://lucid.page/b/{slug}. It 404s while the binder is private.
binders[].item_count requiredinteger—
binders[].version requiredintegerBumped by every change; send it back as expected_version
binders[].created_at requiredstring · date-time—
binders[].updated_at requiredstring · date-time—
binders[].doc_item_idstring | nullOnly when listing with ?doc=: the id of the item holding that page, or null

BinderSummary #

FieldTypeDescription
slug requiredstringDerived from the title at creation; never changes
title requiredstring—
description requiredstring—
visibility requiredstringone of private, unlisted, public
url requiredstringThe binder page, https://lucid.page/b/{slug}. It 404s while the binder is private.
item_count requiredinteger—
version requiredintegerBumped by every change; send it back as expected_version
created_at requiredstring · date-time—
updated_at requiredstring · date-time—
doc_item_idstring | nullOnly when listing with ?doc=: the id of the item holding that page, or null

Binder #

FieldTypeDescription
slug requiredstringDerived from the title at creation; never changes
title requiredstring—
description requiredstring—
visibility requiredstringone of private, unlisted, public
url requiredstringThe binder page, https://lucid.page/b/{slug}. It 404s while the binder is private.
item_count requiredinteger—
version requiredintegerBumped by every change; send it back as expected_version
created_at requiredstring · date-time—
updated_at requiredstring · date-time—
items requiredBinderItem[]In display order
items[].id requiredstring—
items[].kind requiredstringone of page, link
items[].slug requiredstring | nullThe page's slug; null for links
items[].url requiredstringThe page's absolute URL, or the link
items[].title requiredstringThe page's current title, or the link's title (its hostname if none was set)
items[].note requiredstring—
items[].public_visible requiredbooleanWhether the item shows on the binder page. Links always do. A page does while it exists, is not blocked or expired, and is public, or unlisted in an unlisted or private binder.
items[].added_at requiredstring · date-time—

BinderItem #

FieldTypeDescription
id requiredstring—
kind requiredstringone of page, link
slug requiredstring | nullThe page's slug; null for links
url requiredstringThe page's absolute URL, or the link
title requiredstringThe page's current title, or the link's title (its hostname if none was set)
note requiredstring—
public_visible requiredbooleanWhether the item shows on the binder page. Links always do. A page does while it exists, is not blocked or expired, and is public, or unlisted in an unlisted or private binder.
added_at requiredstring · date-time—

ItemInput #

FieldTypeDescription
slugstringA lucid.page page: yours, or any public or unlisted page. Send slug or url, not both.
urlstringAn http or https link. A lucid.page page URL (/{slug}, /{slug}.md or /raw/{slug}) is stored as a page item.
max length 2048
titlestringLink items only (ignored for pages); defaults to the link's hostname
max length 200
notestringmax length 1000

CreateBinderRequest #

FieldTypeDescription
title requiredstringmin length 1 · max length 120
descriptionstringmax length 2000
visibilitystringone of private, unlisted, public · default unlisted
itemsItemInput[]—
items[].slugstringA lucid.page page: yours, or any public or unlisted page. Send slug or url, not both.
items[].urlstringAn http or https link. A lucid.page page URL (/{slug}, /{slug}.md or /raw/{slug}) is stored as a page item.
max length 2048
items[].titlestringLink items only (ignored for pages); defaults to the link's hostname
max length 200
items[].notestringmax length 1000

UpdateBinderRequest #

FieldTypeDescription
expected_version requiredintegerThe binder's current version
titlestringmin length 1 · max length 120
descriptionstringmax length 2000
visibilitystringone of private, unlisted, public

ReorderBinderItemsRequest #

FieldTypeDescription
expected_version requiredintegerThe binder's current version
order requiredstring[]Every current item id, each exactly once, in the new order

UpdateBinderItemRequest #

FieldTypeDescription
notestringmax length 1000
titlestringLink items only; 400 on a page item
max length 200

BillingMe #

FieldTypeDescription
accountId requiredstring—
plan requiredstringone of free, pro, team
activeSubscription requiredboolean—
currentPeriodEndstring | null—
cancelAtstring | null—
portalAvailableboolean—

Error #

FieldTypeDescription
error requiredstring—
codestringMachine-readable code on agent-facing endpoints
hintstring—
docs_urlstring—
current_versionintegerBinder writes: the binder's current version, on 409 and 428