The server API is relatively small. The client primarily communicates with the server for file “CRUD” (Create, Read, Update, Delete) style operations.
All API requests from the client will always set the X-Sync-Mode request header set to true. The server will use this fact to distinguish between requests coming from the client and regular e.g. GET requests from the browser (through navigation) and redirect appropriately (for instance to the UI URL associated with a specific .md file).
When authentication is enabled, most endpoints require a valid session cookie (JWT) or a Bearer token. The following paths are excluded from auth: /service_worker.js, /.client/*, /.auth, /.ping.
GET /.auth: Serves the login page. Returns 403 if authentication is not enabled.POST /.auth: Authenticates a user. Expects form fields: username, password, rememberMe (optional), from (optional redirect path). Returns JSON {"status":"ok","redirect":"..."} on success or {"status":"error","error":"..."} on failure. Implements lockout after repeated failed attempts.GET /.logout: Clears authentication cookies and redirects to /.auth.Authorization: Bearer <token> header instead of cookies.In multi-space mode every account carries a profile — a display name and email, used for attribution — alongside its login credentials. These live under the /.dashboard prefix and require the session cookie described in Dashboard#Dashboard access.
GET /.dashboard/api/profile: The caller’s own profile: {"username", "admin", "fullName", "email"}. fullName/email are null until set. 401 without a session.PUT /.dashboard/api/profile: Sets the caller’s own profile. Body: {"fullName": string, "email": string}. The username always comes from the session — there is no way to address another account through this endpoint. An empty string clears a field. 400 with {"errors":[{"field", "message"}]} if a value contains <, >, a line break, or (for email) whitespace.POST /.dashboard/api/admin/users (admin only): Creates an account. Body: {"username", "password", "admin", "fullName", "email"} — fullName/email are optional, validated the same way.PUT /.dashboard/api/admin/users/<name>/profile (admin only): Sets another account’s profile. Same body and validation as PUT /.dashboard/api/profile.GET /.accounts: Every account with access to this space, as [{"username", "fullName", "me"}], with the caller’s own entry marked me. username is null where the deployment keeps no accounts (the caller is still reported, marked me, so their name is known even though they are nobody the space can address), fullName is omitted when unset, and me is omitted when false.The space file system is exposed under the /.fs prefix. The [[CLI#fs: remote files|sb fs CLI commands]] use these endpoints directly, including metadata and conditional writes, without requiring the Runtime API:
GET /.fs will return a full listing of all files in your space including metadata like when the file was last modified, as well as permissions in JSON format. This is primarily used for sync purposes with the client.GET /.fs/*: Reads and returns the content of the file at the given path. This means that if you GET /.fs/index.md you will receive the content of your index page. If the optional X-Get-Meta request header is set, the server does not need to return the body of the file (but it can). The GET response will have a few additional SB-specific headers:
X-Last-Modified the last modified time of the file as a UNIX timestamp in ms since the epoch (as coming from Date.now()). This timestamp has to match the lastModified listed for this file in /.fs otherwise syncing issues may occur. When this header is missing, frequent polling-based sync will be disabled for this file.X-Created the created time of the file as a UNIX timestamp in ms since the epoch (as coming from Date.now()).X-Permission: either rw or ro which will change whether the editor opens in read-only or edit mode. When missing, ro is assumed.X-Content-Length: which will be the same as Content-Length except if the request was sent with a X-Get-Meta header and the body is not returned (then Content-Length will be 0 and X-Content-Length will be the size of the file)PUT /.fs/*: The same as GET except that it takes the body of the request and writes it to a file.DELETE /.fs/*: Again the same, except this will delete the given file.GET/PUT responses carry an ETag ("sha256:<hash>") reflecting the file’s current content. PUT and DELETE accept the standard conditional headers to guard against overwriting a change you haven’t seen:
If-Match: "sha256:<hash>": only write/delete if the file’s current content still matches that hash.If-None-Match: *: only write if the file doesn’t exist yet (create-only).A precondition that doesn’t hold returns 412 Precondition Failed instead of silently overwriting or deleting whatever is actually there.
This is the same mechanism the built-in clients use to drive the automatic merging and conflict handling described in Collaboration.
Present when Revisions are enabled for the space: every endpoint below answers 404 with {"error": "revisions disabled"} when they are not. A rev is always a full 40-character commit hash — anything else is a 404.
GET /.revisions/: The space-wide commit log as JSON: mode, commits (each with rev, timestamp, author, message, files, added and removed), uncommitted (paths differing from HEAD right now) and more. Takes optional before (page back from this revision), limit (default 50, capped at 200) and q (only commits whose message or author contains this phrase) query parameters.POST /.revisions/: Commits everything outstanding immediately. Takes no body, returns {"committed": true|false}. Returns 409 for a space that is not in Managed mode.GET /.revisions/?rev=<hash>&to=<hash|HEAD|WORKING>: A summary of everything that changed between rev and to as JSON: from, to, files (each with path, status, added and removed) and authors. to=WORKING reaches all the way to what’s on disk, uncommitted changes included.GET /.revisions/<path>: One file’s revisions as JSON: mode, uncommitted (whether what is on disk differs from the last commit), revisions and more. Takes the same before/limit parameters.GET /.revisions/<path>?rev=<hash>: The content of that file as it was at that revision, served with the file’s own content type.GET /.revisions/<path>?rev=<hash>&format=diff: A unified diff of what that revision changed in that file, as text/plain. 404 when the revision has nothing to diff against a parent (a merge commit).GET /.revisions/<path>?format=diff: With no rev, the uncommitted change instead — HEAD versus what is on disk, as text/plain. A file that has never been committed reads as wholly added. 404 when it matches HEAD after all.The server answers Search itself, so these work without the Runtime API. The CLI’s sb search and sb related use them. They need Read access. Public visitors only get the modes the server allows them, and no related pages. Bodies are JSON. Errors are 4xx JSON with an error code and a message.
GET /.search/status: What search can do right now: backend (null when unavailable), model, capabilities (modes, the first being the default, and whether related is available), ready, progress ({"done", "total"} while building) and reason.POST /.search/query: Body {"query", "mode", "scope", "include", "exclude", "limit"}. Only query is required. mode is words, phrase or natural (the Semantic mode), scope is pages, tool or all (meta still works as an alias of tool), limit is at most 50. Returns items (matches, best page first, each with page, pos, line, heading, excerpt and highlights), total, partial and notice (e.g. when semantic search fell back to word matching). With X-Await-Index: 1, a member’s query first lets the index catch up with recent file changes and the response carries X-Index-Status, as described in Runtime API#Waiting for the index.POST /.search/related: Body {"page", "limit", "exclude", "scope"}: pages related in meaning to page, in the same shape as a query’s result. Needs semantic search.GET /.help: What an assistant needs to start working in the space, as JSON with "version": 1: the skills available (pages tagged meta/skill: the built-in Library/Std ones, those of installed libraries and the space’s own, each with its page, description and source), an outline of the space (folders with page counts, a capped list of top-level pages with pageCount, whether an AGENTS page exists, and the tags and attributes used in page frontmatter), and runtime, whether the Runtime API is available to the caller. The CLI’s sb help and the MCP’s help tool render it. It needs a signed-in account or an API token, even on a public space: anonymous visitors get 401 (or the sign-in redirect), although they can read /.fs. While the space’s first scan is still running, scanning is true and only the built-in skills are listed.
Some functionality is exposed as RPC-style calls
GET /.ping: Returns 200 with body OK if the server is available.POST /.shell: Run a shell command on the server side and return the result.POST /.logs: Accepts a JSON array of log entry objects with fields source, level, message, and timestamp. The server logs these with the client’s IP. Returns OK.* /.proxy/<host[:port]>/path: Proxy an HTTP request to avoid CORS issues.
http:// is used for localhost, 127.0.0.1, bare IP addresses, and host.docker.internal. https:// is used for everything else.X-Proxy-Header- have the prefix stripped and are forwarded to the upstream server.x-proxy-header- prefix.x-proxy-status-code header contains the actual upstream HTTP status code. The proxy response itself always returns 200 to avoid interference.Enabled when Chrome/Chromium is available and runtime access is permitted. In single-space mode, SB_RUNTIME_API=0 disables it. In multi-space mode, use the administrator’s APIs controls and per-writer Runtime API permission. See Runtime API for full documentation, setup instructions, and examples.
In multi-space mode, POST /.mcp is a stateless MCP (Model Context Protocol) endpoint that authenticates with an OAuth access token (Authorization: Bearer <token>, issued to a connected app); API tokens are refused with 401 and a WWW-Authenticate challenge pointing at the resource metadata. These endpoints are served on the main router only in multi-space mode, answer 404 unless an admin has turned MCP on (it is off by default) and when SB_MCP=0 (except /.auth/revoke), and are not served in single-space mode:
GET /.well-known/oauth-protected-resource/.mcp (also at /.well-known/oauth-protected-resource): RFC 9728 resource metadata.GET /.well-known/oauth-authorization-server: RFC 8414 authorization server metadata.POST /.auth/register: RFC 7591 dynamic client registration, answers 201. Registered clients are public (token_endpoint_auth_method none).GET /.auth/mcp/authorize: authorization endpoint (PKCE). Signed-out requests redirect to the Dashboard login; the consent page lists the spaces to grant.POST /.auth/mcp/authorize: the consent form's submission (Allow or Cancel). It re-checks every authorization parameter and the CSRF token bound to the signed-in session, then redirects back to the app with a code or access_denied.POST /.auth/mcp/token: authorization code and refresh token grants (refresh tokens rotate). Public clients authenticate with PKCE only. A client whose metadata document registers private_key_jwt must also send client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and an RFC 7523 client_assertion on both grants: signed (RS256, PS256 or ES256) with a key from its jwks_uri or jwks, iss and sub its client ID, aud the token endpoint or issuer URL, valid for at most 5 minutes, with a jti not used before. Failures answer 401 invalid_client.POST /.auth/revoke: RFC 7009 revocation, always 200.See MCP.
GET /.config: Retrieve client configuration, JSON with the following keys:
readOnly: Run the client in read-only modespaceFolderPath: Path of the space (used to prefix client database names to support switching space folders)indexPage: name of the index pagelogPush: whether the client should push logs to the server via /.logsenableClientEncryption: true when authentication is enabled, offering client-side encryption as an optionGET /.client/*: Retrieve files implementing the clientGET /.client/manifest.json: Dynamically generated PWA manifest containing the space name and description (not served from the static bundle).GET /* (every other path) will serve the HTML of the SilverBullet UI