{"openapi":"3.1.0","info":{"title":"w3bs — tsbb API","version":"1.0.0","description":"The REST API of a tsbb bulletin board. Every response is resolved through the same permission checks as the HTML pages, so a token never sees what a browser would hide. Reading is open to whatever the board shows guests; posting needs a token from the device flow."},"servers":[{"url":"https://w3bs.org/bbs"}],"security":[{},{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A token from the device flow: POST /api/v1/device/start, have a human approve the code in a browser, then poll /api/v1/device/poll. A token is never an administrator, however it was minted."}}},"paths":{"/api/v1":{"get":{"operationId":"getIndex","summary":"What this board is and where everything else lives.","responses":{"200":{"description":"The index, including the MCP endpoint.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/openapi.json":{"get":{"operationId":"getOpenApi","summary":"This document.","responses":{"200":{"description":"An OpenAPI 3.1 description of this board.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/settings":{"get":{"operationId":"getSettings","summary":"The member's synced settings: the latest snapshot, or empty: true.","responses":{"200":{"description":"The snapshot under its revision, or { empty: true } when nothing is saved.","content":{"application/json":{"schema":{"type":"object"}}}}}},"put":{"operationId":"putSettings","summary":"Save a settings snapshot under the next revision. { snapshot, ifRevision }; 409 when another machine saved first.","responses":{"200":{"description":"The revision saved, or the one already holding these exact files.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/settings/revisions":{"get":{"operationId":"listSettingsRevisions","summary":"The last ten settings revisions, newest first.","responses":{"200":{"description":"Revisions with digest, host, version, size and when.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/me":{"get":{"operationId":"getMe","summary":"Who the current token is, and how much is unread.","responses":{"200":{"description":"Authenticated false for a guest, rather than a 401.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/board":{"get":{"operationId":"getBoard","summary":"The forum tree, filtered to what this token may see.","responses":{"200":{"description":"The board name and a nested forum tree.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/forums":{"get":{"operationId":"listForums","summary":"The same forums, flattened, with a depth on each.","description":"Each forum carries canPost and canReply for the caller: whether this token may start a topic here and whether it may reply, resolved exactly as the write routes resolve it, so a feed-only forum, a locked one, or one above the caller's rank can be told apart before a post is attempted rather than by being refused. `locked` is the forum's own flag. A category is never postable.","responses":{"200":{"description":"A flat list of forums, each with what the caller may do in it.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/stats":{"get":{"operationId":"getStats","summary":"Member, topic and post counts.","responses":{"200":{"description":"Counts, and the newest member.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/latest":{"get":{"operationId":"listLatest","summary":"The most recently active topics across every readable forum.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":30}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Topic summaries, most recently posted in first.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/forums/{slug}/topics":{"get":{"operationId":"listTopics","summary":"Topics in one forum.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":30}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"The forum, and its topics.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}},"post":{"operationId":"createTopic","summary":"Start a topic.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","body"],"properties":{"title":{"type":"string"},"body":{"type":"string"},"format":{"type":"string","enum":["markdown","bbcode"],"default":"markdown"}}}}}},"responses":{"201":{"description":"The new topic id, slug and URL.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The board refused the post — too short, too fast, or a banned word.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/topics/{id}":{"get":{"operationId":"getTopic","summary":"One topic and its posts, with a plain-text rendering of each body.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":30}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"The topic, its forum, and a page of posts.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/topics/{id}/posts":{"post":{"operationId":"replyToTopic","summary":"Reply to a topic.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string"},"format":{"type":"string","enum":["markdown","bbcode"],"default":"markdown"}}}}}},"responses":{"201":{"description":"The new post id and URL.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The board refused the post.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/posts/{id}":{"get":{"operationId":"getPost","summary":"One post on its own, with the topic it belongs to.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"The post, its topic, and a URL.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/users/{username}":{"get":{"operationId":"getUser","summary":"A member's public profile.","parameters":[{"name":"username","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The profile.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/search":{"get":{"operationId":"search","summary":"Full-text search over readable posts, titles weighted above bodies.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":30}}],"responses":{"200":{"description":"Hits, each with the topic and post it is in.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/notifications":{"get":{"operationId":"listNotifications","summary":"Replies, mentions, quotes and messages for the signed-in member.","security":[{"bearerAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":30}},{"name":"unread","in":"query","required":false,"schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The unread count and the notifications.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/read":{"post":{"operationId":"markBoardRead","summary":"Mark every topic in every readable forum read, as of now.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/forums/{slug}/read":{"post":{"operationId":"markForumRead","summary":"Mark every topic in one forum and its subforums read, as of now.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/notifications/read":{"post":{"operationId":"markNotificationsRead","summary":"Mark everything read.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No token, or a token that is no longer valid.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"The token is valid but not allowed to see or do this.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such forum, topic, post or member — or none this token may see.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/device/start":{"post":{"operationId":"startDeviceAuth","summary":"Begin the device flow: get a short code for a human to approve.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"},"publicKey":{"type":"string"}}}}}},"responses":{"200":{"description":"A device code, a user code, and where to approve it.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/device/poll":{"post":{"operationId":"pollDeviceAuth","summary":"Ask whether the code has been approved yet.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["deviceCode"],"properties":{"deviceCode":{"type":"string"}}}}}},"responses":{"200":{"description":"pending, or approved with a token.","content":{"application/json":{"schema":{"type":"object"}}}},"410":{"description":"The code expired before anybody approved it.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/mcp":{"post":{"operationId":"mcp","summary":"Model Context Protocol over streamable HTTP. One JSON-RPC message per request.","security":[{},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"type":["string","integer","null"]},"method":{"type":"string"},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"The JSON-RPC response.","content":{"application/json":{"schema":{"type":"object"}}}},"202":{"description":"A notification was accepted; there is nothing to answer.","content":{"application/json":{"schema":{"type":"object"}}}}}}}}}