# API Documentation

All protected endpoints require a `Bearer` token in the `Authorization` header.

## Authentication

### Request Sign-In Code
`POST /auth/request-code`

Sends a 4-digit sign-in code to the provided email. Creates user if not exists.

**Request Body:**
```json
{
  "email": "user@example.com"
}
```

**Response:** `204 No Content`

### Sign In
`POST /auth/sign-in`

Verifies the code and returns a session token.

**Request Body:**
```json
{
  "email": "user@example.com",
  "code": "1234"
}
```

**Response:**
```json
{
  "human": { "id": "...", "email": "...", "email_prefix": "...", "created_at": "..." },
  "token": "session_token"
}
```

### Sign Out
`POST /auth/sign-out` (Protected)

Invalidates the current session. Returns `204 No Content`.

### Get Current User
`GET /auth/me` (Protected)

Returns the authenticated user.

---

## Startup

### Get Startup Data
`GET /startup` (Protected)

Bootstrap endpoint for initial app load. Returns all networks the user belongs to, with all streams and their particles fully enriched.

**Response:**
```json
{
  "networks": [
    {
      "id": "net-456",
      "name": "My Team",
      "admin_human": { "id": "...", "email": "...", "email_prefix": "...", "created_at": "..." },
      "humans": [{ "id": "...", "email": "...", "email_prefix": "...", "created_at": "..." }],
      "open_stream_count": 2,
      "open_stream_capacity": 5,
      "created_at": "2025-01-15T10:30:00Z",
      "streams": [
        {
          "id": "p-001",
          "name": "Sprint Planning",
          "description": "Weekly sync",
          "status": "open",
          "members": ["alice@example.com"],
          "particles": [
            {
              "id": "p-002",
              "type": "text",
              "data": { "content": "Hello" },
              "created_by_email": "alice@example.com",
              "seen": true,
              "acks": [],
              "updated_at": "...",
              "created_at": "..."
            }
          ],
          "unseen_count": 0
        }
      ]
    }
  ]
}
```

---

## Networks

### Create Network
`POST /networks` (Protected)

**Request Body:**
```json
{
  "name": "My Network"
}
```

### List Networks
`GET /networks` (Protected)

Returns all networks the user is a member of.

### Get Network
`GET /networks/{id}` (Protected)

Returns a specific network by ID.

### Add Members to Network
`POST /networks/{id}/members` (Protected)

**Request Body:**
```json
{
  "email_addresses": ["user1@example.com", "user2@example.com"]
}
```

### Remove Member from Network
`DELETE /networks/{id}/members/{email}` (Protected)

Removes a member by email from the network.

### Set Open Stream Capacity
`PUT /networks/{id}/capacity` (Protected, Admin only)

**Request Body:**
```json
{
  "capacity": 10
}
```

---

## Streams

Streams are top-level particles of type `stream`. They have dedicated endpoints for creation and management, and contain child particles.

### Create Stream
`POST /networks/{network_id}/streams` (Protected)

**Request Body:**
```json
{
  "name": "Sprint Planning",
  "description": "Weekly sync",
  "visibility": "custom",
  "members": ["user@example.com"]
}
```

- `visibility`: `network_all` (default) or `custom`
- `members` is required when visibility is `custom`

**Response:** `201 Created` — returns a [Stream](#stream-1) object.

### Get Stream
`GET /streams/{id}` (Protected)

Returns a stream with all its child particles, enriched with seen/ack state.

**Response:** returns a [Stream](#stream-1) object.

### Update Stream
`PATCH /streams/{id}` (Protected)

Updates a stream's name and/or description. Status is not affected (use the open/close endpoints instead). Only provided fields are updated.

**Request Body:**
```json
{
  "name": "New Name",
  "description": "New description"
}
```

- Both fields are optional — omit a field to leave it unchanged
- `name` cannot be empty if provided

**Response:** returns the updated [Stream](#stream-1) object.

### Create Stream Particle
`POST /streams/{id}/particles` (Protected)

Creates a child particle inside a stream. Child particles inherit visibility from the stream.

**Request Body:**
```json
{
  "type": "text|media|file|quest|paper",
  "data": {}
}
```

- Cannot create `stream` or `folder` types as children
- For `media` and `file` types, `data` must include a valid `object_id` from depot

**Response:** `201 Created` — returns a [StreamParticle](#streamparticle) object.

### Open Stream
`POST /streams/{id}/open` (Protected)

Opens a closed stream. Fails with `409` if capacity would be exceeded.

### Close Stream
`POST /streams/{id}/close` (Protected)

Closes an open stream.

### Add Members to Stream
`POST /streams/{id}/members` (Protected)

**Request Body:**
```json
{
  "emails": ["user@example.com"]
}
```

### Remove Members from Stream
`DELETE /streams/{id}/members` (Protected)

**Request Body:**
```json
{
  "emails": ["user@example.com"]
}
```

---

## Particles

### List Particles
`GET /networks/{network_id}/particles` (Protected)

**Query Parameters:**
- `parent_id` (optional): Filter by parent particle
- `cursor` (optional): Pagination cursor
- `direction` (optional): `after` or `before` (default: `after`)
- `type` (optional, repeatable): Filter by particle type

**Response enrichment:**
Each particle in the response includes:
- `seen` (boolean): Whether the requester has marked this particle as seen
- `acks` (array): List of acknowledgments `[{email, acked_at}]`
- `unseen_count` (integer, streams only): Count of unseen child particles

### Get Particle
`GET /particles/{id}` (Protected)

### Update Particle
`PATCH /particles/{id}` (Protected)

**Request Body:**
```json
{
  "data": {}
}
```

### Delete Particle
`DELETE /particles/{id}` (Protected)

Deletes the particle and all children. If it references a depot object, that is also deleted.

### Download Particle
`GET /particles/{id}/download` (Protected)

Returns a `302` redirect to a signed download URL. Only works for `media` and `file` particles.

### Mark Seen
`POST /particles/{id}/seen` (Protected)

Marks a particle as seen by the requester. This is private state, only visible to the requester.

Returns `204 No Content` on success.

### Mark Seen (Batch)
`POST /particles/seen` (Protected)

Marks multiple particles as seen by the requester.

**Request Body:**
```json
{
  "particle_ids": ["particle_uuid1", "particle_uuid2"]
}
```

Returns `204 No Content` on success.

### Acknowledge Particle
`POST /particles/{id}/ack` (Protected)

Acknowledges a particle. Acknowledgments are public and permanent, visible to all users with access. Also marks the particle as seen.

Returns `204 No Content` on success.

---

## Depot (File Storage)

### Prepare Upload
`POST /depot/upload` (Protected)

Prepares a signed URL for direct upload to GCS.

**Request Body:**
```json
{
  "network_id": "network_uuid",
  "name": "filename.png",
  "content_type": "image/png",
  "content_length": 12345
}
```

**Response:**
```json
{
  "object_id": "uuid",
  "upload_url": "https://storage.googleapis.com/...",
  "upload_headers": { "Content-Type": "image/png" }
}
```

### Confirm Upload
`POST /depot/objects/{id}/confirm` (Protected)

Confirms that an upload has been completed.

**Response:**
```json
{
  "id": "uuid",
  "name": "filename.png",
  "content_type": "image/png",
  "content_length": 12345,
  "contains_content": true,
  "created_at": "..."
}
```

### Upload Flow

1. `POST /depot/upload` — get signed URL and `object_id`
2. Upload file directly to GCS using the signed URL
3. `POST /depot/objects/{id}/confirm` — mark upload complete
4. Create a `media` or `file` particle with the `object_id` in its data

---

## Object Reference

### Human

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string \| null` | Unique identifier. Null if not yet registered. |
| `email` | `string` | Email address (stable identifier). |
| `email_prefix` | `string` | The local part of the email (before `@`). |
| `created_at` | `string \| null` | ISO 8601 timestamp. Null if not registered. |

```json
{
  "id": "abc-123",
  "email": "alice@example.com",
  "email_prefix": "alice",
  "created_at": "2025-01-15T10:30:00Z"
}
```

### Network

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier. |
| `name` | `string` | Display name of the network. |
| `admin_human` | `Human` | The network administrator. |
| `humans` | `Human[]` | All members of the network (including admin). |
| `open_stream_count` | `integer` | Number of currently open streams. |
| `open_stream_capacity` | `integer` | Maximum number of concurrent open streams (default: 5). |
| `created_at` | `string` | ISO 8601 timestamp. |

```json
{
  "id": "net-456",
  "name": "My Team",
  "admin_human": { "id": "abc-123", "email": "alice@example.com", "email_prefix": "alice", "created_at": "..." },
  "humans": [
    { "id": "abc-123", "email": "alice@example.com", "email_prefix": "alice", "created_at": "..." }
  ],
  "open_stream_count": 2,
  "open_stream_capacity": 5,
  "created_at": "2025-01-15T10:30:00Z"
}
```

### Stream

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier (this is a particle ID). |
| `name` | `string` | Stream name. |
| `description` | `string` | Stream description. |
| `status` | `string` | `"open"`, `"closed"`, or `"unspecified"`. |
| `members` | `string[]` | Emails of stream members. Omitted for `network_all` visibility. |
| `particles` | `StreamParticle[]` | Child particles in the stream. |
| `unseen_count` | `integer` | Number of unseen child particles for the requester. |

```json
{
  "id": "p-001",
  "name": "Sprint Planning",
  "description": "Weekly sync",
  "status": "open",
  "members": ["alice@example.com"],
  "particles": [],
  "unseen_count": 0
}
```

### StreamParticle

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier. |
| `type` | `string` | One of: `media`, `file`, `text`, `quest`, `paper`. |
| `data` | `object` | Type-specific payload (see [Particle Data by Type](#particle-data-by-type)). |
| `created_by_email` | `string` | Email of the creator. |
| `seen` | `boolean` | Whether the requester has seen this particle. |
| `acks` | `AckInfo[]` | Acknowledgments from users. |
| `updated_at` | `string` | ISO 8601 timestamp. |
| `created_at` | `string` | ISO 8601 timestamp. |

### Particle

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier. |
| `type` | `string` | One of: `stream`, `folder`, `media`, `file`, `text`, `quest`, `paper`. |
| `network_id` | `string` | The network this particle belongs to. |
| `parent_id` | `string \| null` | Parent particle ID, if nested. |
| `created_by_email` | `string` | Email of the creator. |
| `visibility` | `string` | `"network_all"`, `"custom"`, or `"inherited"`. |
| `stream_status` | `string \| null` | Only on `stream` type: `"open"` or `"closed"`. |
| `data` | `object` | Type-specific payload (see [Particle Data by Type](#particle-data-by-type)). |
| `download_url` | `string \| null` | Signed download URL. Only on `media`/`file` particles. |
| `seen` | `boolean \| null` | Whether the requester has seen this particle. Only in list responses. |
| `acks` | `AckInfo[]` | Acknowledgments. Only in list responses. |
| `unseen_count` | `integer \| null` | Unseen child count. Only on `stream` particles in list responses. |
| `updated_at` | `string` | ISO 8601 timestamp. |
| `created_at` | `string` | ISO 8601 timestamp. |

### AckInfo

| Field | Type | Description |
|-------|------|-------------|
| `email` | `string` | Email of the user who acknowledged. |
| `acked_at` | `string` | ISO 8601 timestamp of the acknowledgment. |

### ParticleList

Returned by `GET /networks/{network_id}/particles`.

| Field | Type | Description |
|-------|------|-------------|
| `particles` | `Particle[]` | Array of enriched particle objects. |
| `has_more` | `boolean` | Whether more results exist beyond this page. |
| `next_cursor` | `string \| null` | Cursor to fetch the next page. |
| `prev_cursor` | `string \| null` | Cursor to fetch the previous page. |

### DepotObject

Returned by `POST /depot/objects/{id}/confirm`.

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique object identifier. |
| `name` | `string` | Original filename. |
| `content_type` | `string` | MIME type (e.g. `image/png`). |
| `content_length` | `integer` | Size in bytes. |
| `contains_content` | `boolean` | Whether the object has been uploaded successfully. |
| `created_at` | `string` | ISO 8601 timestamp. |

---

## Particle Data by Type

The `data` field on a Particle is a JSON object whose schema depends on the particle's `type`.

### `stream`

| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Stream name (required). |
| `status` | `string` | `"open"` or `"closed"` (required). |
| `description` | `string \| null` | Stream description (optional). |

```json
{ "name": "Sprint Planning", "status": "open", "description": "Weekly sync" }
```

### `folder`

| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Folder name (required). |
| `color` | `string \| null` | Display color (optional). |

```json
{ "name": "Design Assets", "color": "#FF5733" }
```

### `media`

| Field | Type | Description |
|-------|------|-------------|
| `object_id` | `string` | Reference to depot storage object (required). |
| `mime_type` | `string` | MIME type of the media (required). |
| `duration_ms` | `integer` | Duration in milliseconds, must be > 0 (required). |

```json
{ "object_id": "obj-123", "mime_type": "image/jpeg", "duration_ms": 5000 }
```

### `file`

| Field | Type | Description |
|-------|------|-------------|
| `object_id` | `string` | Reference to depot storage object (required). |
| `filename` | `string` | Original filename (required). |
| `mime_type` | `string` | MIME type (required). |
| `size` | `integer` | File size in bytes, must be > 0 (required). |

```json
{ "object_id": "obj-456", "filename": "report.pdf", "mime_type": "application/pdf", "size": 204800 }
```

### `text`

| Field | Type | Description |
|-------|------|-------------|
| `content` | `string` | Text content (required). |

```json
{ "content": "Hello world" }
```

### `quest`

| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Quest/task title (required). |
| `description` | `string` | Details about the quest (required). |
| `status` | `string \| null` | Current status (optional, e.g. `"todo"`, `"in_progress"`, `"done"`). |
| `assigned_to` | `string \| null` | Email of the assigned user (optional). |
| `due_date` | `string \| null` | ISO date string (optional, e.g. `"2025-03-01"`). |

```json
{ "title": "Fix login bug", "description": "Login fails on Safari", "status": "todo", "assigned_to": "bob@example.com" }
```

### `paper`

| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Document title (required). |
| `content` | `string` | Document body in markdown (required). |

```json
{ "title": "Architecture RFC", "content": "## Overview\n..." }
```

---

## Visibility

Particles support three visibility modes:

| Mode | Description |
|------|-------------|
| `network_all` | Visible to all network members. |
| `custom` | Visible only to specified members (requires `members` list). |
| `inherited` | Inherits visibility from parent particle. Used for child particles in streams. |

Root-level particles (streams, folders) use `network_all` or `custom`. Child particles created via `POST /streams/{id}/particles` automatically use `inherited`.

---

## Error Responses

All endpoints return standard HTTP status codes with plain text error bodies:

- **400** Bad Request — invalid input or missing required fields
- **401** Unauthorized — missing or invalid auth token
- **403** Forbidden — user lacks permission (not a member, not creator, not admin)
- **404** Not Found — resource doesn't exist
- **409** Conflict — state conflict (stream already open/closed, capacity exceeded)
- **500** Internal Server Error
