{"openapi":"3.1.0","info":{"title":"LeafPage API","version":"1.0.0","description":"Publish a self-contained HTML report and receive a permanent URL. Reports are immutable snapshots, served from a cookieless content host.\n\nThe portal, authentication, and API live on the application host `https://leafpage.cc`. Published reports are served from the content host `https://view.leafpage.cc`. All requests and responses are JSON."},"servers":[{"url":"https://leafpage.cc","description":"Application / API host"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Reports","description":"Create, list, and manage HTML report snapshots."},{"name":"Meta","description":"Public, unauthenticated metadata."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"lp_…","description":"Personal access token. Created in the portal under the account menu → API Tokens; the plaintext value is shown once at creation. Scopes: `reports:read` (list), `reports:write` (upload/modify, implies read), `reports:delete`. A token lacking the required scope returns 403."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable error message."}},"required":["error"],"example":{"error":"login required"}},"File":{"type":"object","required":["path","content"],"properties":{"path":{"type":"string","description":"Bare filename (no subdirectories). Exactly one entry must be `index.html`.","example":"index.html"},"content":{"type":"string","description":"File contents. UTF-8 text, or base64 when `encoding` is `base64`."},"encoding":{"type":"string","enum":["utf-8","base64"],"default":"utf-8","description":"Content encoding."}}},"Owner":{"type":"object","properties":{"slug":{"type":"string","example":"acme"},"kind":{"type":"string","enum":["user","org"]},"display_name":{"type":"string","nullable":true}}}}},"paths":{"/api/upload":{"post":{"tags":["Reports"],"summary":"Upload a report","operationId":"uploadReport","description":"Uploads a report snapshot. Requires the `reports:write` scope. Reports are immutable — a new upload to an existing `name` produces a new `code` and version rather than altering an existing one.\n\nLimits: up to 100 files per request, 10 MB per decoded file, 20 MB total request body. Assets reference each other with relative paths; absolute `src=\"/…\"` references and `<base>` are unsupported and reported in `warnings`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["files"],"properties":{"files":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/File"},"description":"Non-empty. One entry must have `path` equal to `index.html`."},"name":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$","description":"Group name. Re-uploading an existing name adds a version; a name owned by another account returns 409."},"title":{"type":"string","maxLength":300,"description":"Defaults to the document's <title> when omitted."},"folder":{"type":"string","description":"Portal-only label, e.g. `RD/2026/Q2`. Never appears in URLs."},"visibility":{"type":"string","enum":["public","private"],"default":"private","description":"Defaults to `private`. A public report is the one served from the content host."}}},"example":{"name":"quarterly-report","visibility":"public","files":[{"path":"index.html","content":"<!doctype html><title>Q2</title><h1>Hello</h1>"}]}}}},"responses":{"200":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","description":"16-char immutable identifier."},"url":{"type":"string","format":"uri","description":"Permanent share URL on the content host."},"owner":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]},"created_at":{"type":"string","format":"date-time"},"name":{"type":"string","description":"Present when a group name was given."},"warnings":{"type":"array","items":{"type":"string"}}}},"example":{"code":"a1b2c3d4e5f6g7h8","url":"https://view.leafpage.cc/a1b2c3d4e5f6g7h8/","owner":"acme","visibility":"public","created_at":"2026-06-27T00:00:00.000Z","name":"quarterly-report"}}}},"400":{"description":"Malformed request: invalid JSON, missing index.html, invalid name/encoding, or invalid field value.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication required or not recognized.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The token lacks the `reports:write` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The group name is owned by another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"A file exceeds the per-file size limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"507":{"description":"A storage quota is full.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/list":{"get":{"tags":["Reports"],"summary":"List reports","operationId":"listReports","description":"Lists the caller's owners, groups, and standalone reports. Requires `reports:read`.","responses":{"200":{"description":"The caller's reports.","content":{"application/json":{"schema":{"type":"object","properties":{"owners":{"type":"array","items":{"$ref":"#/components/schemas/Owner"}},"groups":{"type":"array","items":{"type":"object"}},"standalone":{"type":"array","items":{"type":"object"}},"signedIn":{"type":"boolean"}}},"example":{"owners":[{"slug":"acme","kind":"org","display_name":"Default"}],"groups":[{"name":"quarterly-report","current_code":"a1b2c3d4e5f6g7h8","owner_slug":"acme","latest_title":"Q2 Summary","version_count":3,"visibility":"public","folder":"RD/2026/Q2","created_at":"2026-06-27T00:00:00.000Z"}],"standalone":[],"signedIn":true}}}},"401":{"description":"Authentication required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The token lacks the `reports:read` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/group":{"get":{"tags":["Reports"],"summary":"Get a group","operationId":"getGroup","description":"Returns a named group's current code and version history. Requires `reports:read`.","parameters":[{"name":"name","in":"query","required":true,"schema":{"type":"string"},"example":"quarterly-report"}],"responses":{"200":{"description":"The group.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"current_code":{"type":"string"},"versions":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"title":{"type":"string"},"version_no":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}}}}},"example":{"name":"quarterly-report","current_code":"a1b2c3d4e5f6g7h8","versions":[{"code":"a1b2c3d4e5f6g7h8","title":"Q2 Summary","version_no":3,"created_at":"2026-06-27T00:00:00.000Z"},{"code":"0f9e8d7c6b5a4321","title":"Q2 draft","version_no":2,"created_at":"2026-06-20T00:00:00.000Z"}]}}}},"404":{"description":"The group does not exist or is owned by another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/rename":{"post":{"tags":["Reports"],"summary":"Rename a group","operationId":"renameGroup","description":"Renames a group. Requires `reports:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","newName"],"properties":{"name":{"type":"string"},"newName":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,62}$"}}},"example":{"name":"quarterly-report","newName":"q2-report"}}}},"responses":{"200":{"description":"Renamed.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"name":{"type":"string"}}},"example":{"ok":true,"name":"q2-report"}}}},"404":{"description":"Not found or owned by another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The new name collides with an existing name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/set-folder":{"post":{"tags":["Reports"],"summary":"Set folder","operationId":"setFolder","description":"Moves a group (by `name`) or a standalone report (by `code`) into a portal folder. An empty `folder` unfiles it. Requires `reports:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"code":{"type":"string"},"folder":{"type":"string"}},"description":"Provide either `name` (group) or `code` (standalone), plus `folder`."},"example":{"name":"quarterly-report","folder":"RD/2026/Q2"}}}},"responses":{"200":{"description":"Moved.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"folder":{"type":"string","nullable":true}}},"example":{"ok":true,"folder":"RD/2026/Q2"}}}},"404":{"description":"Not found or owned by another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/set-visibility":{"post":{"tags":["Reports"],"summary":"Set visibility","operationId":"setVisibility","description":"Sets a group (by `name`) or report (by `code`) to `public` or `private`. For a group, every version is updated together. Requires `reports:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["visibility"],"properties":{"name":{"type":"string"},"code":{"type":"string"},"visibility":{"type":"string","enum":["public","private"]}}},"example":{"name":"quarterly-report","visibility":"public"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"visibility":{"type":"string","enum":["public","private"]}}},"example":{"ok":true,"visibility":"public"}}}},"400":{"description":"`visibility` must be `public` or `private`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found or owned by another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/setlatest":{"post":{"tags":["Reports"],"summary":"Set latest version","operationId":"setLatest","description":"Points a group at one of its existing versions. Requires `reports:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","code"],"properties":{"name":{"type":"string"},"code":{"type":"string"}}},"example":{"name":"quarterly-report","code":"a1b2c3d4e5f6g7h8"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"name":{"type":"string"},"current_code":{"type":"string"}}},"example":{"ok":true,"name":"quarterly-report","current_code":"a1b2c3d4e5f6g7h8"}}}},"400":{"description":"The code does not belong to this group.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found or owned by another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/delete-code":{"post":{"tags":["Reports"],"summary":"Delete a version","operationId":"deleteCode","description":"Deletes a single report version. Requires `reports:delete`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string"}}},"example":{"code":"a1b2c3d4e5f6g7h8"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"deleted":{"type":"boolean"}}},"example":{"ok":true,"deleted":true}}}},"403":{"description":"The token lacks the `reports:delete` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/delete-group":{"post":{"tags":["Reports"],"summary":"Delete a group","operationId":"deleteGroup","description":"Deletes a group and all its versions. Requires `reports:delete`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}},"example":{"name":"quarterly-report"}}}},"responses":{"200":{"description":"Done.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"deleted":{"type":"integer","description":"Number of versions deleted."}}},"example":{"ok":true,"deleted":3}}}},"403":{"description":"The token lacks the `reports:delete` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found or owned by another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/config":{"get":{"tags":["Meta"],"summary":"Public config","operationId":"getConfig","description":"Public, unauthenticated. Returns the content host base URL used to build share links.","security":[],"responses":{"200":{"description":"Config.","content":{"application/json":{"schema":{"type":"object","properties":{"publicBaseUrl":{"type":"string","format":"uri"}}},"example":{"publicBaseUrl":"https://view.leafpage.cc"}}}}}}}}}