# 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