{"openapi":"3.1.0","info":{"title":"neverlogoff read-only API","version":"1.0.0-preview","description":"Read the public pile, lenses and public profiles. GET only; nothing here writes anywhere.\n\n- **Public by default.** Without a token you get what a signed-out stranger may see.\n- **As a person.** With `Authorization: Bearer <read token>` (minted by that person at\n  `POST /api/lens?op=token`, 60 minutes, scopes `items` · `lenses` · `people`) you get what THEY may\n  see — decided per item by the same access check the site uses. A bad or expired token is refused\n  (401); it is never quietly treated as signed out.\n- **Cookies are never read.** That is why CORS is open (`*`) and why there is no\n  `Access-Control-Allow-Credentials`. Token answers are `Cache-Control: private, no-store`.\n- **18+.** neverlogoff is for people 18 and over. Every response carries `audience: \"18+\"` and the\n  `X-NLO-Audience: 18+` header. Show your readers that notice before showing items. Explicit adult\n  content is banned on neverlogoff.\n- **Rate limits.** 120 requests a minute per IP without a token, 600 a minute per token. 429 with\n  `Retry-After`. `X-RateLimit-Limit` / `X-RateLimit-Remaining` on every answer.\n- **Pagination.** Opaque `cursor`; `limit` up to 50; `next_cursor` + a `Link: rel=\"next\"` header.\n- **Caching.** Weak `ETag` on every 200; send `If-None-Match` for a 304.\n- **Sorting is open.** Every list of items says how it is sorted (`ranking.line`, `ranking.how`).\n  There is no hidden ranking: recipes are `newest`, `nearest`, `saved_by_people_i_follow`,\n  `random_daily`, `verified_first`, `vouched`, `trending` (most different people saved it in public this week).\n"},"servers":[{"url":"https://neverlogoff.com"}],"security":[{},{"readToken":[]}],"components":{"securitySchemes":{"readToken":{"type":"http","scheme":"bearer","description":"nlou1 read token (type nlo-read-agent). POST /api/lens?op=token while signed in, body {scopes:[items,lenses,people]}."}},"parameters":{"cursor":{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque; from next_cursor."},"limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}},"recipe":{"name":"recipe","in":"query","schema":{"$ref":"#/components/schemas/Recipe"},"description":"Re-sort this view with another open recipe."},"place":{"name":"place","in":"query","schema":{"type":"string"},"description":"A vocabulary place id (gn:… / nlo:rgn:…) for the nearest recipe. We never read your location."}},"headers":{"ETag":{"schema":{"type":"string"}},"XRateLimitLimit":{"schema":{"type":"integer"}},"XRateLimitRemaining":{"schema":{"type":"integer"}},"XNLOAudience":{"schema":{"type":"string","const":"18+"}}},"responses":{"NotFound":{"description":"Missing, private, or not yours to see — the same answer for all three.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"The token did not open (token_malformed, token_bad_signature, token_expired, token_wrong_type, token_bad_scope).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"insufficient_scope — the token lacks the scope this endpoint needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"rate_limited. Wait Retry-After seconds.","headers":{"Retry-After":{"schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotModified":{"description":"The ETag matched."}},"schemas":{"Recipe":{"type":"string","enum":["newest","nearest","saved_by_people_i_follow","random_daily","verified_first","vouched","trending"]},"Envelope":{"type":"object","properties":{"ok":{"type":"boolean"},"audience":{"type":"string","const":"18+"},"notice":{"type":"string"},"as":{"type":"string","enum":["public","token"]}}},"Error":{"type":"object","required":["ok","error"],"properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string"},"audience":{"type":"string"}}},"Ranking":{"type":"object","properties":{"recipe":{"$ref":"#/components/schemas/Recipe"},"params":{"type":"object"},"label":{"type":"string"},"line":{"type":"string","examples":["Sorted by: newest"]},"how":{"type":"string","description":"Plain words for what the recipe does and what it never uses."}}},"Signed":{"type":"object","description":"The head of the signed record ledger. Verify with the issuer keys at /.well-known/nlo-pass-keys.json (nlo_use \"item\").","properties":{"item_id":{"type":"string"},"version":{"type":"integer"},"envelope":{"type":"object","properties":{"payload":{"type":"object"},"hash":{"type":"string"},"sig":{"type":"object","properties":{"alg":{"type":"string","const":"Ed25519"},"kid":{"type":"string"},"value":{"type":"string"}}},"chunks":{"type":"array","description":"Only on single-item reads."},"chunks_omitted":{"type":"integer","description":"In lists","how many chunks were left out.":null}}}}},"Item":{"type":"object","required":["key","kind","source","created_at","why"],"properties":{"key":{"type":"string","examples":["post:p_0123456789abcdef0123","fed:bluesky:did:plc:…/3k…"]},"kind":{"type":"string","enum":["post","fed"]},"source":{"type":"object","properties":{"id":{"type":"string","enum":["nlo","bluesky","youtube"]},"label":{"type":"string","examples":["neverlogoff","Bluesky","YouTube"]}}},"created_at":{"type":"string","format":"date-time"},"owner":{"type":["string","null"],"description":"The creator's PUBLIC id on neverlogoff (32 hex)."},"why":{"type":"array","items":{"type":"string"},"description":"Why it matched, e.g. \"ramen ⊂ food\"."},"rank_why":{"description":"Why it sits where it does, e.g. {saves: 3} or {km: 12} or \"signed_verified\"."},"coin":{"type":"boolean","description":"A link to a coin page. Hidden for regions where coins are blocked."},"post":{"type":"object","description":"The post as a stranger (or the token's person) may see it. Owner-only fields are absent."},"link":{"type":"object","description":"Items from connected accounts. Text and a link out only; their media is never copied or proxied.","properties":{"url":{"type":"string"},"text":{"type":"string"},"format":{"type":"string"},"rehosted":{"type":"boolean","const":false}}},"signed":{"$ref":"#/components/schemas/Signed"}}},"Lens":{"type":"object","properties":{"id":{"type":"string","pattern":"^l_[a-f0-9]{20}$"},"rev":{"type":"integer"},"owner":{"type":"string"},"owner_name":{"type":["string","null"]},"name":{"type":"string"},"description":{"type":"string"},"query":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"expand":{"type":"boolean"}}}},"places":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"expand":{"type":"boolean"}}}},"formats":{"type":"array","items":{"type":"string","enum":["video","photo","carousel","text","link"]}},"languages":{"type":"array","items":{"type":"string"}},"sources":{"type":"array","items":{"type":"string","enum":["nlo","bluesky","youtube","mastodon","rss"]}},"people":{"type":"array","items":{"type":"string"}},"spaces":{"type":"array","items":{"type":"string"}},"date":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"last_days":{"type":"integer"}}},"feeds":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"url":{"type":"string"}}}}}},"ranking":{"type":"object","properties":{"recipe":{"$ref":"#/components/schemas/Recipe"},"params":{"type":"object"}}},"ranking_explained":{"$ref":"#/components/schemas/Ranking"},"visibility":{"type":"string","enum":["only_me","approved","followers","public"]},"derived_from":{"type":["object","null"],"description":"A remix credit: {id, owner, name, rev, credit}."},"meta":{"$ref":"https://neverlogoff.com/schemas/nlo-meta-v0.json"},"signed":{"$ref":"#/components/schemas/Signed"}}}}},"paths":{"/api/v1/items":{"get":{"summary":"Search the pile (an unsaved lens)","description":"Items newest first unless you pick a recipe. Requires scope `items` when a token is sent.","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Free text; words are resolved to topic and place ids."},{"name":"tags","in":"query","schema":{"type":"string"},"description":"Comma-separated topic ids."},{"name":"places","in":"query","schema":{"type":"string"},"description":"Comma-separated place ids."},{"name":"expand","in":"query","schema":{"type":"string","enum":["0","1"],"default":"1"},"description":"Include narrower concepts."},{"name":"formats","in":"query","schema":{"type":"string"}},{"name":"languages","in":"query","schema":{"type":"string"}},{"name":"people","in":"query","schema":{"type":"string"},"description":"Comma-separated public ids."},{"name":"sources","in":"query","schema":{"type":"string","default":"nlo"}},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}},{"$ref":"#/components/parameters/recipe"},{"$ref":"#/components/parameters/place"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"A page of items.","headers":{"ETag":{"$ref":"#/components/headers/ETag"},"X-NLO-Audience":{"$ref":"#/components/headers/XNLOAudience"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Envelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Item"}},"total":{"type":"integer"},"next_cursor":{"type":["string","null"]},"ranking":{"$ref":"#/components/schemas/Ranking"}}}]}}}},"304":{"$ref":"#/components/responses/NotModified"},"400":{"description":"bad_cursor, unknown_tag, wrong_tag_kind, bad_format, …","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/items/{id}":{"get":{"summary":"One item, with its full signed record","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"post:p_… or p_…"}],"responses":{"200":{"description":"The item.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Envelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Item"}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"451":{"description":"region_blocked — a coin item","from a region where coins are blocked.":null}}}},"/api/v1/lenses":{"get":{"summary":"Find lenses","description":"Public, listed lenses (by topic, place or words). With `owner`, that person's lenses you may see. Scope `lenses`.","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"owner","in":"query","schema":{"type":"string"}},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"Lenses.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Envelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Lens"}},"total":{"type":"integer"},"mode":{"type":"string"}}}]}}}}}}},"/api/v1/lenses/{id}":{"get":{"summary":"One lens, with its signed record","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The lens.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Envelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Lens"}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/lenses/{id}/items":{"get":{"summary":"What a lens shows (for you, or for a stranger)","description":"Scopes `lenses` and `items`. `client_side` lists RSS/Mastodon feeds the lens names — fetch those yourself; neverlogoff never does.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/recipe"},{"$ref":"#/components/parameters/place"},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"A page of items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Envelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Item"}},"total":{"type":"integer"},"next_cursor":{"type":["string","null"]},"ranking":{"$ref":"#/components/schemas/Ranking"},"client_side":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"source_label":{"type":"string"},"url":{"type":"string"},"fetched_by":{"type":"string","const":"your_browser"}}}}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/people/{id}/public":{"get":{"summary":"A person's public profile","description":"Name, verified public accounts, public lenses, how many public items, how many public vouches. Scope `people`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{32}$"}}],"responses":{"200":{"description":"The profile."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/recipes":{"get":{"summary":"Every open ranking recipe, in plain words","responses":{"200":{"description":"{data:[{id, label, line, how, inputs, params}]}"}}}},"/api/v1/vocab":{"get":{"summary":"The topic and place vocabulary","description":"`q` autocompletes; `id` returns one concept with its broader and narrower concepts. No scope needed.","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"id","in":"query","schema":{"type":"string"}},{"name":"kinds","in":"query","schema":{"type":"string","default":"place,topic"}}],"responses":{"200":{"description":"Suggestions or one concept."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/search":{"get":{"summary":"Run a lens spec (anyone's own search, as JSON) or a built-in named lens","description":"A lens spec is the lens record's `query` + `ranking`, plus `kinds`, `creators` (@names), `limit`, `cursor`,\n`include_examples`. Unknown fields and recipes are refused; cost caps: limit 50, 24 filter values, 365-day\nwindow, 4 KB. Format: GET /api/v1/specs. Built-in lenses are named specs: new, this_week, trending, picked\n(following is planned → 501). Example items are never returned; include_examples adds a marked pointer to /examples.\n","parameters":[{"name":"spec","in":"query","schema":{"type":"string"},"description":"The spec as JSON (URL-encoded)."},{"name":"named","in":"query","schema":{"type":"string","enum":["new","this_week","trending","picked","following"]}},{"name":"examples","in":"query","schema":{"type":"string","enum":["1"]}},{"$ref":"#/components/parameters/cursor"}],"responses":{"200":{"description":"{data:[Item + provenance], total, next_cursor, ranking, lens_rule:{recipe, line, how, spec_hash}, spec, examples, unresolved_creators?}"},"304":{"$ref":"#/components/responses/NotModified"},"400":{"description":"spec_required, bad_json, spec_too_big, unknown_field, unknown_recipe, unknown_kind, unknown_tag, bad_limit, window_too_wide, too_many_facets, bad_creator, …","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"unknown_lens_name"},"429":{"$ref":"#/components/responses/RateLimited"},"501":{"description":"planned: a named lens that is not built yet (following)"}}}},"/api/v1/trending":{"get":{"summary":"The Trending named lens","description":"Things from the last 7 days, ordered by how many DIFFERENT people saved them in public in that window (one count\nper person; the maker's own save not counted), then newest. Never lists examples, held items or coin items. Reads\npublic saves only: no views, watch time, paid boost, private saves or reports. Rule: lib/nlo-discover-trending-v0.cjs.\n","parameters":[{"name":"tags","in":"query","schema":{"type":"string"},"description":"Comma-separated topic ids."},{"name":"places","in":"query","schema":{"type":"string"}},{"name":"kinds","in":"query","schema":{"type":"string"},"description":"post, app, game, template, …"},{"name":"creator","in":"query","schema":{"type":"string"},"description":"Comma-separated @names."},{"name":"examples","in":"query","schema":{"type":"string","enum":["1"]}},{"$ref":"#/components/parameters/cursor"},{"$ref":"#/components/parameters/limit"}],"responses":{"200":{"description":"Same shape as /api/v1/search."},"304":{"$ref":"#/components/responses/NotModified"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/specs":{"get":{"summary":"The lens spec format, caps, kinds, recipes, named specs and the trending rule","responses":{"200":{"description":"{data:{format, caps, kinds, recipes, named, trending, saved_as}}"}}}},"/api/v1/normalise":{"get":{"summary":"Words to vocabulary ids (case, '#', plurals and the vocabulary's own synonyms fold)","parameters":[{"name":"tags","in":"query","schema":{"type":"string"},"description":"Comma-separated words."},{"name":"kind","in":"query","schema":{"type":"string","enum":["topic","place","any"],"default":"topic"}}],"responses":{"200":{"description":"{data:{tags:[id], suggested:[{input, candidates:[{id, kind, label}]}]}} — unknown or ambiguous words are suggestions, never tags"}}}},"/api/v1/openapi.json":{"get":{"summary":"This description, as JSON (generated from docs/api/v1.yaml; scripts/smoke-openapi-v1-v0.cjs fails if they drift)","responses":{"200":{"description":"The OpenAPI 3.1 document. No scope needed."}}}}},"x-source":{"file":"docs/api/v1.yaml","sha256":"83eddaa44602e6adee13188debb49d08f8a8e2ba8b527a3633b3b8e16646c8d0","generated_by":"scripts/build-openapi-v1-v0.py"}}