16 KiB
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:
{
"email": "user@example.com"
}
Response: 204 No Content
Sign In
POST /auth/sign-in
Verifies the code and returns a session token.
Request Body:
{
"email": "user@example.com",
"code": "1234"
}
Response:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"name": "Sprint Planning",
"description": "Weekly sync",
"visibility": "custom",
"members": ["user@example.com"]
}
visibility:network_all(default) orcustommembersis required when visibility iscustom
Response: 201 Created — returns a Stream object.
Get Stream
GET /streams/{id} (Protected)
Returns a stream with all its child particles, enriched with seen/ack state.
Response: returns a Stream 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:
{
"name": "New Name",
"description": "New description"
}
- Both fields are optional — omit a field to leave it unchanged
namecannot be empty if provided
Response: returns the updated Stream 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:
{
"type": "text|media|file|quest|paper",
"data": {}
}
- Cannot create
streamorfoldertypes as children - For
mediaandfiletypes,datamust include a validobject_idfrom depot
Response: 201 Created — returns a 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:
{
"emails": ["user@example.com"]
}
Remove Members from Stream
DELETE /streams/{id}/members (Protected)
Request Body:
{
"emails": ["user@example.com"]
}
Particles
List Particles
GET /networks/{network_id}/particles (Protected)
Query Parameters:
parent_id(optional): Filter by parent particlecursor(optional): Pagination cursordirection(optional):afterorbefore(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 seenacks(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:
{
"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:
{
"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:
{
"network_id": "network_uuid",
"name": "filename.png",
"content_type": "image/png",
"content_length": 12345
}
Response:
{
"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:
{
"id": "uuid",
"name": "filename.png",
"content_type": "image/png",
"content_length": 12345,
"contains_content": true,
"created_at": "..."
}
Upload Flow
POST /depot/upload— get signed URL andobject_id- Upload file directly to GCS using the signed URL
POST /depot/objects/{id}/confirm— mark upload complete- Create a
mediaorfileparticle with theobject_idin 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. |
{
"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. |
{
"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. |
{
"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). |
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). |
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). |
{ "name": "Sprint Planning", "status": "open", "description": "Weekly sync" }
folder
| Field | Type | Description |
|---|---|---|
name |
string |
Folder name (required). |
color |
string | null |
Display color (optional). |
{ "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). |
{ "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). |
{ "object_id": "obj-456", "filename": "report.pdf", "mime_type": "application/pdf", "size": 204800 }
text
| Field | Type | Description |
|---|---|---|
content |
string |
Text content (required). |
{ "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"). |
{ "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). |
{ "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