# 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: <https://api.sharebriefs.com/user/briefs?per_page=10&page=2>; rel="next",
      <https://api.sharebriefs.com/user/briefs?per_page=10&page=1>; rel="prev",
      <https://api.sharebriefs.com/user/briefs?per_page=10&page=1>; rel="first",
      <https://api.sharebriefs.com/user/briefs?per_page=10&page=5>; 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": "<!doctype 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 `<title>`/`<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                                              |






