# Briefs REST API Base URL: `https://api.sharebriefs.com` All endpoints require a Bearer token (Personal Access Token) in the `Authorization` header: ``` Authorization: Bearer sb_... ``` ## Pagination All list endpoints support page-based pagination via query parameters: | Parameter | Default | Range | Description | | ---------- | ------- | ------ | --------------------------- | | `per_page` | 30 | 1-100 | Number of items per page | | `page` | 1 | 1+ | Page number (1-indexed) | Paginated responses include a `Link` header with navigation URLs: ``` Link: ; rel="next", ; rel="prev", ; rel="first", ; rel="last" ``` | Rel | Included when | | ------- | -------------------- | | `next` | Not on the last page | | `prev` | Not on the first page| | `first` | Always (when results exist) | | `last` | Always (when results exist) | --- ## Reading a public Brief without a token You do not need this API to read a **public** Brief — just fetch its web URL. The Briefs host content-negotiates the same `https://sharebriefs.com/{owner}/{name}` URL: - A programmatic fetch (`Accept: */*`, which is the default for `fetch`/`curl`), a `.md` suffix (`https://sharebriefs.com/{owner}/{name}.md`), or `?format=md` returns `text/markdown` — the raw Brief. - A browser (`Accept: text/html`) gets the rendered HTML page. No token is required. Private and internal Briefs return `404` on this URL (existence is never leaked) — read those through the authenticated API below or the MCP server, which carry the bearer token that proves access. This is read-only. Creating, updating, deleting, and sharing all go through the authenticated endpoints below. --- ## Users ### Get the authenticated user ``` GET /user ``` **Response:** `200 OK` ```json { "data": { "id", "username", "email", "createdAt" } } ``` ### Get a user ``` GET /users/{username} ``` Returns public profile information. Returns `404` if the username does not exist or belongs to an organization. **Response:** `200 OK` ```json { "data": { "id", "username", "createdAt" } } ``` --- ## Briefs ### List my briefs ``` GET /user/briefs GET /briefs ``` **Response:** `200 OK` ```json { "data": [{ "id", "name", "fullName", "url", "ownerType", "ownerId", "ownerNamespace", "visibility", "permissions": { "admin": true, "write": true, "read": true }, "createdAt", "updatedAt" }] } ``` ### List user's briefs ``` GET /users/{username}/briefs ``` Returns only **public** briefs. If the authenticated user is the specified user, returns all briefs including private ones. ### List org's briefs ``` GET /orgs/{org}/briefs ``` Returns briefs visible to the authenticated user: - **Org admins** see all briefs - **Org members** see public, internal, and private briefs where they are a collaborator - **Non-members** see only public briefs ### Create a brief ``` POST /user/briefs POST /briefs POST /users/{username}/briefs # must be your username POST /orgs/{org}/briefs # must be org member ``` **Body:** ```json { "name": "my-brief", "content": "# My Brief\n\nContent here...", "visibility": "public" } ``` | Field | Required | Description | | ------------ | -------- | -------------------------------------------------------------- | | `content` | Yes | Markdown body | | `name` | No | Brief identifier. Generated from content heading if omitted | | `visibility` | No | `"public"` (default), `"private"`, or `"internal"` (org only) | The `name` field is lowercased, spaces replaced with hyphens, and must be unique per owner. Returns `422` if the name is already taken. **Response:** `201 Created` ```json { "data": { "id", "name", "fullName", "url", "content", "visibility" } } ``` ### Get a brief ``` GET /briefs/{owner}/{name} ``` **Response:** `200 OK` ```json { "data": { "id", "name", "fullName", "url", "content", "ownerType", "ownerId", "ownerNamespace", "visibility", "permissions": { "admin": true, "write": true, "read": true }, "createdBy", "updatedBy", "createdAt", "updatedAt" } } ``` ### Update a brief ``` PATCH /briefs/{owner}/{name} ``` **Body:** (at least one field required) ```json { "content": "updated content", "name": "new-name", "visibility": "private" } ``` | Field | Permission Required | Description | | ------------ | ------------------- | --------------------------------- | | `content` | `write` | Update markdown body | | `name` | `write` | Rename (must be unique per owner) | | `visibility` | `admin` | Change access level | **Response:** `200 OK` ### Delete a brief ``` DELETE /briefs/{owner}/{name} ``` Requires `admin` permission. **Response:** `204 No Content` --- ## Collaborators ### List collaborators ``` GET /briefs/{owner}/{name}/collaborators ``` Requires `read` access to the brief. **Response:** `200 OK` ```json { "data": [{ "id", "username", "permission", "addedAt" }] } ``` ### Check a user's permission ``` GET /briefs/{owner}/{name}/collaborators/{username}/permission ``` Requires `read` access to the brief. **Response:** `200 OK` ```json { "data": { "username": "alice", "permission": "write" } } ``` ### Add or update collaborator If the user is already a collaborator, updates their permission directly. Otherwise, creates an invitation they must accept. ``` PUT /briefs/{owner}/{name}/collaborators/{username} ``` Requires `admin` permission. **Body:** ```json { "permission": "write" } ``` | Value | Access | | ------- | ---------------------------------- | | `read` | View only | | `write` | View + write content | | `admin` | Full control (delete, manage, etc) | **Response:** - `201 Created` — invitation sent ```json { "data": { "id", "briefId", "briefName", "briefFullName", "invitedBy", "targetUsername", "permission" } } ``` - `204 No Content` — already a collaborator, permission updated ### Remove collaborator ``` DELETE /briefs/{owner}/{name}/collaborators/{username} ``` Requires `admin` permission. **Response:** `204 No Content` --- ## Brief Invitations (admin view) ### List pending invitations ``` GET /briefs/{owner}/{name}/invitations ``` Requires `admin` permission. **Response:** `200 OK` ```json { "data": [{ "id", "briefId", "briefFullName", "briefOwnerNamespace", "briefName", "invitedBy", "targetUid", "targetUsername", "permission", "expired", "createdAt", "expiresAt" }] } ``` ### Update invitation permission Update the permission level on a pending invitation before it is accepted. ``` PATCH /briefs/{owner}/{name}/invitations/{inviteId} ``` Requires `admin` permission. **Body:** ```json { "permission": "write" } ``` **Response:** `200 OK` ```json { "data": { "id": "...", "permission": "write" } } ``` ### Cancel invitation ``` DELETE /briefs/{owner}/{name}/invitations/{inviteId} ``` Requires `admin` permission. **Response:** `204 No Content` --- ## Brief Invitations (invitee view) ### List my pending brief invitations ``` GET /user/brief_invitations ``` **Response:** `200 OK` ```json { "data": [{ "id", "briefId", "briefFullName", "briefOwnerNamespace", "briefName", "invitedBy", "targetUid", "targetUsername", "permission", "createdAt", "expiresAt" }] } ``` ### Accept a brief invitation ``` PATCH /user/brief_invitations/{inviteId} ``` Returns `422` if the invitation has expired. **Response:** `204 No Content` ### Decline a brief invitation ``` DELETE /user/brief_invitations/{inviteId} ``` **Response:** `204 No Content` --- ## Pages Pages are single-file HTML documents viewed and shared at `https://sharebriefs.com/{owner}/pages/{name}`. They share the brief ownership, visibility, and collaborator model — the endpoints below mirror briefs with `/pages` in place of `/briefs`. See https://sharebriefs.com/brief/pages. ### List pages ``` GET /users/{username}/pages # public pages; all of yours if it is you GET /orgs/{org}/pages # visible pages per your org role ``` ### Create a page ``` POST /users/{username}/pages # must be your username POST /orgs/{org}/pages # must be org member ``` **Body:** ```json { "html": "...", "name": "my-page", "visibility": "public" } ``` | Field | Required | Description | | ------------ | -------- | ------------------------------------------------------------- | | `html` | Yes | A single HTML document (≤ 5 MB; inline all CSS/JS/assets) | | `name` | No | Generated from the ``/`<h1>` if omitted | | `visibility` | No | `"public"` (default), `"private"`, or `"internal"` (org only) | **Response:** `201 Created` ```json { "data": { "id", "name", "fullName", "url", "renderUrl", "visibility", "storagePath", "sizeBytes", "createdAt", "updatedAt" } } ``` Returns `413 CONTENT_TOO_LARGE` if the HTML exceeds 5 MB. The returned `url` is the shareable web URL (`https://sharebriefs.com/{owner}/pages/{name}`, works for every visibility); `renderUrl` is the raw public render origin (`pages.sharebriefs.com`, serves public Pages only). ### Get a page ``` GET /pages/{owner}/{name} ``` Requires `read` access. Returns metadata plus the raw `html` and the viewer's `permissions`. ### Update a page ``` PATCH /pages/{owner}/{name} ``` **Body:** (at least one field) ```json { "html": "...", "name": "new-name", "visibility": "private" } ``` `html` and `name` require `write`; `visibility` requires `admin`. **Response:** `200 OK` ### Delete a page ``` DELETE /pages/{owner}/{name} ``` Requires `admin`. **Response:** `204 No Content` ### Get a render URL (private/internal pages) ``` GET /pages/{owner}/{name}/render-url ``` Runs the same access check as a read, then returns a short-lived (~10 minute) signed URL to the page's HTML — used to render a non-public page in a sandboxed frame. Public pages render for anyone at the web URL and don’t need this. Returns `404` for both missing and forbidden (existence never leaks). **Response:** `200 OK` ```json { "data": { "url": "https://storage.googleapis.com/...", "expiresAt": "2026-..." } } ``` ### Page collaborators & invitations Identical to briefs, with `/pages/{owner}/{name}/...` in place of `/briefs/{owner}/{name}/...` — see the Collaborators and Brief Invitations sections above. --- ## Organizations ### List my orgs ``` GET /user/orgs ``` **Response:** `200 OK` ```json { "data": [{ "id", "namespace", "name", "role", "joinedAt" }] } ``` ### Create an org ``` POST /user/orgs ``` **Body:** ```json { "namespace": "my-org", "name": "My Organization" } ``` **Response:** `201 Created` ```json { "data": { "id", "namespace", "name" } } ``` ### Get org details ``` GET /orgs/{org} ``` Returns public info for any authenticated user. Members see additional fields including settings and their role. **Response (non-member):** `200 OK` ```json { "data": { "id", "namespace", "name", "createdAt" } } ``` **Response (member):** `200 OK` ```json { "data": { "id", "namespace", "name", "createdBy", "defaultBriefPermission", "membersCanCreate", "role", "createdAt", "updatedAt" } } ``` ### Update org settings ``` PATCH /orgs/{org} ``` Requires `admin` role. **Body:** ```json { "membersCanCreate": true, "defaultBriefPermission": "read" } ``` | Field | Values | | ------------------------ | ----------------------- | | `membersCanCreate` | `true` / `false` | | `defaultBriefPermission` | `"none"` / `"read"` / `"write"` | **Response:** `200 OK` ### Delete org ``` DELETE /orgs/{org} ``` Requires `admin` role. Cascades: deletes all members, memberships, briefs, and invites. **Response:** `204 No Content` --- ## Org Members ### List members ``` GET /orgs/{org}/members ``` Requires org membership. **Response:** `200 OK` ```json { "data": [{ "id", "username", "role", "joinedAt" }] } ``` Roles: `"admin"` (full control) or `"member"`. ### Add or update member If the user is already a member, updates their role. Otherwise, creates an invitation they must accept. ``` PUT /orgs/{org}/members/{username} ``` Requires `admin` role. **Body:** ```json { "role": "member" } ``` | Value | Access | | -------- | ----------------------------------------------- | | `admin` | Full org control: settings, members, briefs | | `member` | Access briefs per org defaults (default if omitted) | Cannot demote the last admin. **Response:** - `201 Created` — invitation sent ```json { "data": { "id": "..." } } ``` - `204 No Content` — existing member's role updated ### Remove member ``` DELETE /orgs/{org}/members/{username} ``` Requires `admin` role. Cannot remove the last admin. **Response:** `204 No Content` --- ## Org Invitations (admin view) ### List pending org invitations ``` GET /orgs/{org}/invitations ``` Requires `admin` role. **Response:** `200 OK` ```json { "data": [{ "id", "orgId", "orgName", "orgNamespace", "invitedBy", "targetUid", "targetUsername", "role", "expired", "createdAt", "expiresAt" }] } ``` ### Cancel org invitation ``` DELETE /orgs/{org}/invitations/{inviteId} ``` Requires `admin` role. **Response:** `204 No Content` --- ## Org Memberships (invitee view) ### List my org memberships ``` GET /user/memberships/orgs GET /user/memberships/orgs?state=active GET /user/memberships/orgs?state=pending ``` Without a `state` filter, returns both active memberships and pending invitations. **Response:** `200 OK` ```json { "data": [ { "state": "active", "role": "admin", "organization": { "id", "namespace", "name" } }, { "state": "pending", "role": "member", "organization": { "id", "namespace", "name" }, "inviteId": "...", "invitedBy": "...", "createdAt": "..." } ] } ``` ### Get an org membership ``` GET /user/memberships/orgs/{org} ``` Where `{org}` is the org namespace. Returns `404` if the user has no relationship with the org. **Response (active):** `200 OK` ```json { "data": { "state": "active", "role": "admin", "organization": { "id", "namespace", "name" } } } ``` **Response (pending):** `200 OK` ```json { "data": { "state": "pending", "role": "member", "organization": { "id", "namespace", "name" }, "inviteId": "...", "invitedBy": "...", "createdAt": "..." } } ``` ### Accept org invitation ``` PATCH /user/memberships/orgs/{org} ``` Where `{org}` is the org namespace. **Body:** ```json { "state": "active" } ``` Returns `422` if the invitation has expired. **Response:** `200 OK` ```json { "data": { "state": "active", "role": "member", "organization": { "id", "namespace", "name" } } } ``` ### Decline invitation or leave org ``` DELETE /user/memberships/orgs/{org} ``` Where `{org}` is the org namespace. If the user has a pending invitation, declines it. If the user is an active member, leaves the org. Cannot leave if you are the last admin. **Response:** `204 No Content` --- ## Error Responses All errors follow this format: ```json { "error": { "code": "NOT_FOUND", "message": "Not found" } } ``` | Status | Code | Meaning | | ------ | --------------- | --------------------------------------------------------- | | 400 | `BAD_REQUEST` | Missing or invalid parameters | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `FORBIDDEN` | Insufficient permissions | | 404 | `NOT_FOUND` | Resource not found | | 422 | `UNPROCESSABLE` | Valid request but cannot process (e.g., duplicate invite) | | 500 | `INTERNAL` | Server error |