API

Base URL: https://readwithlumen.app. JSON in, JSON out. Errors are always { "error": { "code": "snake_case", "message": "Human sentence." } }.

Authentication

Caller How Notes
Web app lumen_session cookie (HttpOnly, SameSite Lax) Writes must send Origin of this site, or X-Lumen: 1
Extension, scripts Authorization: Bearer lum_… Create tokens in Integrations; revoke in Settings. CORS allows extension origins only

Auth routes

Method Path Purpose
GET /auth/providers { google, github, demo }: which sign in options are configured
GET /auth/:provider/start?next=/app Starts OAuth 2 with PKCE (google or github)
GET /auth/:provider/callback Exchanges the code, links or creates the user, sets the session, redirects to next
POST /auth/demo Creates a seeded sandbox user and a 3 day session
POST /auth/logout Ends the session

Account

Method Path Body / query Returns
GET /api/v1/me { user, via }
PATCH /api/v1/me { name?, dailyGoal?, timezone?, aiProvider? } { user }
DELETE /api/v1/me cookie only deletes everything
GET /api/v1/stats streak, best streak, glow today, goal, total, 7 day series, counts, level
GET /api/v1/tags [{ name, count }]
GET /api/v1/tokens token metadata (never the secret), with client, calls and lastTool for agents
POST /api/v1/tokens { name }, cookie only { id, token } shown once
DELETE /api/v1/tokens/:id
GET /api/v1/export everything as a JSON download

Items

Method Path Body / query Notes
GET /api/v1/items status (inbox, reading, done, archived, all), collection, tag, q, favorite=1, kind, cursor, limit ≤ 100 Keyset paginated { data, nextCursor }; archived hidden unless asked
GET /api/v1/items/lookup?url= { item, highlights } for a page (tracking params ignored)
POST /api/v1/items { url, html?, title?, collectionId?, tags?, status?, kind? } Idempotent per URL: returns the existing item with created: false. With html (extension) the server extracts from it; without, it fetches the URL
GET /api/v1/items/:id item with contentHtml + highlights
PATCH /api/v1/items/:id { status?, progress?, favorite?, collectionId?, tags?, title? } done sets finished_at and awards glow once
DELETE /api/v1/items/:id cascades highlights

Highlights

Method Path Body / query Notes
POST /api/v1/items/:id/highlights { quote, prefix, suffix, position, color, note? } colour: a palette name (sun, peach, rose, grape, iris, sky, teal, mint, lime, slate) or any #rrggbb (also #rgb or bare rrggbb); moves inbox items to reading
GET /api/v1/highlights color (a name, a hex, or custom for every hex colour), item, collection, notes=1, q, cursor, limit ≤ 200 each includes item { id, title, url, siteName }
PATCH /api/v1/highlights/:id { color?, note? } first note awards glow; an unknown colour is 422 bad_color
DELETE /api/v1/highlights/:id

Collections

Method Path Body Notes
GET /api/v1/collections with itemCount, highlightCount
POST /api/v1/collections { name, emoji?, color?, description?, destinationId? } collection colours stay sun, mint, iris, peach, sky
PATCH /api/v1/collections/:id any of the above, destinationId: null to detach
DELETE /api/v1/collections/:id items stay, collection_id cleared
GET /api/v1/collections/:id/export.md?format=house|callout|plain the whole collection as one note

Destinations and delivery

Method Path Body Notes
GET /api/v1/destinations with pendingCount, deliveredCount, lastDeliveredAt
POST /api/v1/destinations { kind, name, config, secret? } see configs below
PATCH /api/v1/destinations/:id { name?, config?, secret? }
DELETE /api/v1/destinations/:id
GET /api/v1/destinations/:id/pending { highlightIds, markdown, groups }: only highlights this destination has not received
POST /api/v1/destinations/:id/ack { highlightIds, error? } records delivery; idempotent
POST /api/v1/destinations/:id/push server delivery for notion and webhook

Configs:

  • obsidian: { notePath, baseUrl = https://127.0.0.1:27124, format = house }. The API key stays on the client.
  • notion: { pageId } (a link is accepted) and secret = integration token.
  • markdown: { fileName, format }.
  • webhook: { url } (https) and optional secret for X-Lumen-Signature: sha256=<hmac>.

Formats: house = ==quote==^[note] ([title](link)), callout = Obsidian > [!quote], plain = blockquote. Every link is a text fragment URL (#:~:text=) that scrolls to the passage.

Webhook payload:

{ "type": "highlights.created",
  "highlights": [{ "id": "hl_…", "quote": "…", "note": null,
                   "source": { "title": "…", "url": "…", "link": "…#:~:text=…" } }] }

Discover

Method Path Body / query Returns
GET /api/v1/reads?period=daily|weekly { picks, resurface, trending, minutes, budgetMinutes }: a plan that fits the daily goal
GET /api/v1/reminders items in progress, idle 20+ hours, with minutesLeft and idleDays
POST /api/v1/items/:id/snooze { days } (1 to 30) hides the reminder until then
GET /api/v1/radar { topics, trends, lastRunAt }; refreshes if older than 6 hours
POST /api/v1/radar/refresh forces a refresh
POST /api/v1/topics { name } follow a topic
DELETE /api/v1/topics/:name mute a topic (auto topics come from tags)
POST /api/v1/trends/:id/save saves the story to the library
POST /api/v1/trends/:id/dismiss hides it
GET /api/v1/similar readers like you: URLs saved by 2+ readers whose libraries overlap with yours
GET /api/v1/analytics 84 day heatmap, weekly minutes and words, topics, sites, colours, hours, totals

GET /api/v1/items/:id also returns alsoSaving: how many readers like you saved the same URL (0 when fewer than 2). PATCH /api/v1/me accepts shareSignals and reminders (booleans). POST /api/v1/tokens accepts kind: "agent".

Following (favourite blogs)

Method Path Body / query Notes
GET /api/v1/following followed blogs with unseen counts; stale feeds refresh in the background
POST /api/v1/following { url } (blog or feed) finds the RSS or Atom feed (link tags, then common paths); free plan follows up to 20
GET /api/v1/following/lookup?url= is this site followed
PATCH /api/v1/following/:id { notify?, title? }
DELETE /api/v1/following/:id
GET /api/v1/following/posts unseen=1, feed, limit newest first, with saved
POST /api/v1/following/posts/:id/seen, /api/v1/following/seen { feedId? } mark read
POST /api/v1/following/refresh check now
POST /api/v1/following/notify new posts not announced yet, marked announced (used by extension notifications)

Feeds are checked by Cron every 6 hours (oldest first) and whenever Following is opened.

Shares

Method Path Body / query Notes
GET /api/v1/shares your public share links
POST /api/v1/shares { kind: highlight|progress|collection, refId, title, payload, image? } snapshot of what you chose to share plus a 1200x630 JPEG or PNG data URL (900 KB max); reuses the link for the same kind and refId
DELETE /api/v1/shares/:id the public page stops working

Public: GET /s/:id (page with Open Graph tags and a "Read with Lumen" pill) and GET /s/:id/og.jpg (the preview image). Cards are drawn in the browser on a canvas; only the snapshot and the link image are stored.

MCP

POST /mcp speaks MCP over Streamable HTTP with a Bearer key. See MCP.md.

AI

Method Path Body Returns
POST /api/v1/ai/run { action: explain|summarize|define|simplify|expand|ask, text, question?, title?, context? } text/event-stream: data: {"response":"…"} … data: [DONE]
POST /api/v1/ai/chat { question, page?: { title, url, text }, selection?, history? } SSE: first {"sources":[…]} (numbered library matches), then {"response":"…"} chunks; answers cite sources as [n]
POST /api/v1/ai/spark { data: Idea[] } from the last 30 highlights
GET /api/v1/ideas ideas not dismissed
PATCH /api/v1/ideas/:id { status: new|saved|dismissed } keeping an idea awards glow

Daily AI limits: demo 40, Reader 100, Pro 1000 (429 ai_quota).

Glow rules

Action Glow
Save 1
Highlight 2
Note (first on a highlight) 3
Finish an item 10
Keep a Spark idea 2

Levels: Spark 0, Candle 50, Lantern 200, Lighthouse 600, Aurora 1500, Sun 4000.

Error codes

bad_json, unauthorized, csrf, bad_origin, not_found, bad_url, empty_quote, nothing_to_update, no_collection, no_destination, bad_kind, client_delivered, demo, push_failed, ai_quota, ai_unavailable, not_enough, spark_unparsed, internal.