2026-07-16 06:19:58 -04:00
# API Documentation
2026-08-10 08:53:18 +00:00
Project E exposes a REST API built with [Hono ](https://hono.dev/ ) running on [Bun ](https://bun.sh/ ). All endpoints live under `/api/` and exchange JSON.
The API server listens on port 3001 (Docker) and is proxied from the Vite dev server on port 3000. In development, `http://localhost:3000/api/...` reaches the same routes as `http://localhost:3001/api/...` .
2026-07-16 06:19:58 -04:00
## Table of Contents
2026-08-10 08:53:18 +00:00
- [Overview ](#overview )
2026-07-16 06:19:58 -04:00
- [Authentication ](#authentication )
2026-08-10 08:53:18 +00:00
- [Request and Response Format ](#request-and-response-format )
2026-07-16 06:19:58 -04:00
- [Error Handling ](#error-handling )
2026-08-10 08:53:18 +00:00
- [Pagination and Sorting ](#pagination-and-sorting )
- [Domain Scoping ](#domain-scoping )
- [Health ](#health )
- [Auth ](#auth )
- [Domains ](#domains )
- [Tasks ](#tasks )
- [Habits ](#habits )
- [Projects ](#projects )
- [Notes ](#notes )
- [Search ](#search )
- [Calendar ](#calendar )
- [Graph ](#graph )
- [Dashboard ](#dashboard )
- [Agents ](#agents )
- [Webhooks ](#webhooks )
- [Canvas ](#canvas )
- [Daily Notes ](#daily-notes )
- [Tags ](#tags )
- [Custom Fields ](#custom-fields )
- [Error Log ](#error-log )
- [Analytics ](#analytics )
- [Notifications ](#notifications )
- [Export and Import ](#export-and-import )
- [Realtime (SSE) ](#realtime-sse )
- [MCP ](#mcp )
## Overview
| Item | Value |
|------|-------|
| Framework | Hono, served by Bun (`bun run dev` / the API Docker image) |
| Base path | `/api` |
| Body format | JSON (`Content-Type: application/json` ) |
| Auth | Cookie session, JWT bearer token, or API key |
| Database | PostgreSQL via Drizzle ORM |
All routes mount through `apps/api/src/index.ts` :
```
/api/auth /api/tasks /api/habits /api/projects
/api/notes /api/search /api/calendar /api/graph
/api/dashboard /api/agents /api/webhooks /api/canvas
/api/daily-notes /api/tags /api/custom-fields
/api/error-log /api/analytics /api/notifications
/api/export /api/import /api/realtime /api/mcp
/api/health
```
2026-07-16 06:19:58 -04:00
## Authentication
2026-08-10 08:53:18 +00:00
Every endpoint except `GET /api/health` requires authentication. The API accepts three credential forms, in this order of resolution:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
1. **Session cookie** named `session` , set by `POST /api/auth/credentials` . The cookie is `httpOnly` , `SameSite=Lax` , and persists for 30 days. The JWT inside it expires after 7 days.
2. **Bearer token** via the `Authorization: Bearer <token>` header. The token can be the same JWT issued at login, or an API key.
3. **API key** in the `Authorization: Bearer <apiKey>` header. Keys are validated against the `api_keys` table, which stores only the SHA-256 hash of the key.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
API keys are the required credential for `POST /api/mcp` (JSON-RPC). The MCP endpoint does not accept cookie or JWT auth.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Credentials login
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`POST /api/auth/credentials` with `{ email, password }` . On success the server sets the `session` cookie and returns the user plus the JWT:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```bash
curl -X POST http://localhost:3000/api/auth/credentials \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"your-password"}' \
-c cookies.txt
2026-07-16 06:19:58 -04:00
```
```json
{
"user" : {
2026-08-10 08:53:18 +00:00
"id" : "a1b2c3d4-..." ,
2026-07-16 06:19:58 -04:00
"email" : "user@example.com" ,
"name" : "User Name"
},
2026-08-10 08:53:18 +00:00
"token" : "eyJhbGciOiJIUzI1NiJ9..."
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
First login auto-creates the admin account. When the `users` table is empty, the request is accepted only if `email` matches `INITIAL_ADMIN_EMAIL` and `password` matches `INITIAL_ADMIN_PASSWORD` from the environment. The created account uses `INITIAL_ADMIN_NAME` or the email as its display name.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Invalid credentials return `401 UNAUTHORIZED` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Session check
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /api/auth/session` returns the current session state. It never errors on an unauthenticated request:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{ "authenticated" : true , "user" : { "id" : "..." , "email" : "..." , "name" : "..." } }
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{ "authenticated" : false , "user" : null }
2026-07-16 06:19:58 -04:00
```
2026-08-10 08:53:18 +00:00
### Current user
`GET /api/auth/me` returns the authenticated user or `401 UNAUTHORIZED` :
2026-07-16 06:19:58 -04:00
```json
2026-08-10 08:53:18 +00:00
{ "user" : { "id" : "..." , "email" : "..." , "name" : "..." } }
2026-07-16 06:19:58 -04:00
```
### Logout
2026-08-10 08:53:18 +00:00
`POST /api/auth/logout` clears the `session` cookie and returns `{ "success": true }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Removed features
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Passkey authentication was removed. The legacy passkey routes issued a session without verifying the WebAuthn signature, which was an authentication bypass. Do not re-add passkey endpoints without full WebAuthn challenge/attestation verification.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Request and Response Format
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Write endpoints (`POST` , `PATCH` , `PUT` ) expect a JSON body with `Content-Type: application/json` . Bodies are validated with Zod schemas. Invalid input returns `400 VALIDATION_ERROR` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Success status codes:
2026-07-16 06:19:58 -04:00
| Status | Meaning |
|--------|---------|
| `200` | Success |
| `201` | Created |
2026-08-10 08:53:18 +00:00
| `204` | Deleted (no body) |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Responses use camelCase field names (`domainId` , `createdAt` , `deletedAt` ). Timestamps are ISO 8601 strings with timezone.
2026-07-16 06:19:58 -04:00
## Error Handling
2026-08-10 08:53:18 +00:00
Errors use a consistent envelope:
2026-07-16 06:19:58 -04:00
```json
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Invalid input" ,
"details" : [
{
"code" : "invalid_string" ,
"message" : "Title is required" ,
"path" : [ "title" ]
}
]
}
}
```
2026-08-10 08:53:18 +00:00
`details` appears only when present (mostly Zod validation issues). Error codes:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Code | Status | Meaning |
|------|--------|---------|
| `UNAUTHORIZED` | 401 | Missing or invalid credentials |
| `VALIDATION_ERROR` | 400 | Request body or params failed validation |
2026-07-16 06:19:58 -04:00
| `NOT_FOUND` | 404 | Resource does not exist |
2026-08-10 08:53:18 +00:00
| `FORBIDDEN` | 403 | Missing workspace ID, or the workspace belongs to someone else |
| `CONFLICT` | 409 | State conflict (reserved; not currently returned by routes) |
2026-07-16 06:19:58 -04:00
| `INTERNAL_ERROR` | 500 | Unexpected server error |
2026-08-10 08:53:18 +00:00
Two endpoints deviate from the envelope:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
- `GET /api/realtime` returns `401` with `{ "error": "Unauthorized" }` when unauthenticated.
- `POST /api/mcp` returns JSON-RPC error objects (see [MCP ](#mcp )).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Pagination and Sorting
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Most list endpoints paginate with `page` and `perPage` :
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Parameter | Type | Default | Max | Description |
|-----------|------|---------|-----|-------------|
| `page` | number | 1 | - | Page number, 1-indexed |
| `perPage` | number | 50 | 100 | Items per page |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Response shape:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{
"items" : [],
"totalItems" : 42 ,
"totalPages" : 3 ,
"page" : 1 ,
"perPage" : 50
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Tasks, habits, projects, and notes also accept `limit` and `offset` (default limit 50, max 200). These four endpoints echo both styles in the response (`page` , `perPage` , `limit` , `offset` ).
Sorting uses `sort=<field>` with a leading `-` for descending:
2026-07-16 06:19:58 -04:00
```bash
2026-08-10 08:53:18 +00:00
GET /api/tasks?sort= -due_date
2026-07-16 06:19:58 -04:00
```
2026-08-10 08:53:18 +00:00
Some endpoints (tasks, habits, projects, notes) accept a separate `order=asc|desc` parameter that flips the sort direction.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Endpoints that return everything for a domain (calendar events, graph nodes, custom fields, notifications, agent activity) skip pagination and return `{ items, totalItems }` or `{ items, count }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Domain Scoping
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Every entity belongs to a domain (workspace). All by-ID routes verify the caller owns the entity's domain through `requireWorkspaceAccess` and return `403 FORBIDDEN` (missing or foreign workspace) or `404 NOT_FOUND` (no such workspace) otherwise.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
List endpoints accept `?domain=<id>` to scope results. When omitted, they resolve the user's active domain: the user's first domain by `sortOrder` then `createdAt` , or a "Personal" domain auto-created on first use.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Health
2026-07-16 06:19:58 -04:00
#### `GET /api/health`
2026-08-10 08:53:18 +00:00
No authentication required. Pings the database and reports runtime state:
2026-07-16 06:19:58 -04:00
```json
{
"status" : "ok" ,
2026-08-10 08:53:18 +00:00
"timestamp" : "2026-08-10T10:30:00.000Z" ,
"version" : "0.1.0" ,
"runtime" : "bun" ,
"uptime" : 12345.6 ,
"database" : {
"connected" : true ,
"ping_ms" : 4
}
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`status` is `ok` when the DB ping succeeds and `degraded` when it fails. `database.ping_ms` is `-1` on failure.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Auth
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/auth/credentials` | Log in with email and password; sets `session` cookie |
| `GET` | `/api/auth/session` | Return `{ authenticated, user }` |
| `GET` | `/api/auth/me` | Return the current user |
| `POST` | `/api/auth/logout` | Clear the session cookie |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Domains
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Domains are workspaces. They are owned by a single user (`ownerId` ).
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/domains` | List the caller's domains |
| `POST` | `/api/domains` | Create a domain |
| `GET` | `/api/domains/:id` | Get a domain |
| `PATCH` | `/api/domains/:id` | Update a domain |
| `DELETE` | `/api/domains/:id` | Delete a domain |
**List query parameters:** `page` , `perPage` , `sort` (`name` , `slug` , `sort_order` , `created_at` , `updated_at` ; default `sort_order` ), `filter` (case-insensitive match on name or slug).
**Create:**
```bash
curl -X POST http://localhost:3000/api/domains \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{"name":"Work","slug":"work","color":"#3b82f6"}'
2026-07-16 06:19:58 -04:00
```
2026-08-10 08:53:18 +00:00
`name` is required. `slug` defaults to a slugified name. Response (`201` ):
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"id" : "b2c3d4e5-..." ,
"name" : "Work" ,
"slug" : "work" ,
"color" : "#3b82f6" ,
"icon" : null ,
"ownerId" : "a1b2c3d4-..." ,
"parentId" : null ,
"sortOrder" : 0 ,
"customFields" : {},
"createdAt" : "2026-08-10T10:30:00.000Z" ,
"updatedAt" : "2026-08-10T10:30:00.000Z"
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`PATCH /api/domains/:id` accepts any subset of `name` , `slug` , `color` , `icon` , `parentId` . `DELETE` returns `204` and hard-deletes the row (domains have no `deleted_at` column).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Tasks
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Task statuses: `todo` , `in_progress` , `done` , `cancelled` . Priorities: `low` , `medium` , `high` , `urgent` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/tasks` | List tasks |
| `POST` | `/api/tasks` | Create a task |
| `POST` | `/api/tasks/reorder` | Persist Kanban column ordering |
| `GET` | `/api/tasks/:id` | Get a task with subtasks, tags, dependencies |
| `PATCH` | `/api/tasks/:id` | Update a task |
| `DELETE` | `/api/tasks/:id` | Soft-delete a task |
| `POST` | `/api/tasks/:id/status` | Change task status (Kanban drag) |
| `POST` | `/api/tasks/:id/tags` | Assign a tag |
| `DELETE` | `/api/tasks/:id/tags/:tagId` | Remove a tag |
| `GET` | `/api/tasks/:id/history` | Status change log (from activity feed) |
| `GET` | `/api/tasks/:id/comments` | List comments |
| `POST` | `/api/tasks/:id/comments` | Add a comment |
| `GET` | `/api/tasks/:id/attachments` | List attachment metadata |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### List tasks
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:**
2026-07-16 06:19:58 -04:00
| Parameter | Type | Description |
|-----------|------|-------------|
2026-08-10 08:53:18 +00:00
| `domain` | string | Workspace ID; defaults to active domain |
| `search` | string | Case-insensitive match on title |
| `filter` | string | Case-insensitive match on title or description |
| `status` | string | Comma-separated statuses |
| `priority` | string | Comma-separated priorities |
| `tag` | string | Comma-separated tag IDs (task must have all) |
| `parent_id` | string | Filter by parent; `null` for root tasks |
| `project_id` | string | Filter by project |
| `section_id` | string | Filter by project section |
| `sort` | string | `created` , `updated` , `title` , `status` , `priority` , `order` , `due_date` (default `-created` ) |
| `order` | string | `asc` or `desc` |
| `page` / `perPage` | number | Pagination (default 1 / 50) |
| `limit` / `offset` | number | Alternative pagination (max limit 200) |
2026-07-16 06:19:58 -04:00
**Example:**
```bash
2026-08-10 08:53:18 +00:00
curl "http://localhost:3000/api/tasks?status=todo,in_progress&sort=due_date&domain=<domainId>" \
-b cookies.txt
2026-07-16 06:19:58 -04:00
```
```json
{
"items" : [
{
2026-08-10 08:53:18 +00:00
"id" : "c3d4e5f6-..." ,
2026-07-16 06:19:58 -04:00
"title" : "Complete documentation" ,
"description" : "Write API docs" ,
"status" : "todo" ,
"priority" : "high" ,
2026-08-10 08:53:18 +00:00
"domainId" : "b2c3d4e5-..." ,
"projectId" : null ,
"sectionId" : null ,
"parentId" : null ,
"dueDate" : "2026-08-20T00:00:00.000Z" ,
"completedAt" : null ,
"estimatedMinutes" : null ,
"trackedMinutes" : 0 ,
"recurrenceRule" : null ,
"order" : 0 ,
"customFields" : {},
"createdAt" : "2026-08-10T10:30:00.000Z" ,
"updatedAt" : "2026-08-10T10:30:00.000Z" ,
"tags" : []
2026-07-16 06:19:58 -04:00
}
],
"totalItems" : 1 ,
"totalPages" : 1 ,
"page" : 1 ,
2026-08-10 08:53:18 +00:00
"perPage" : 50 ,
"limit" : 50 ,
"offset" : 0
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
### Create a task
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`title` is required. `domain` defaults to the active domain. Other accepted fields: `description` , `status` , `priority` , `projectId` , `sectionId` , `parentId` , `dueDate` (ISO 8601), `estimatedMinutes` , `order` , `customFields` , `recurrenceRule` (RRule string; the worker spawns the next occurrence), `tagIds` .
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"title" : "Ship the release" ,
"description" : "Cut the v0.2.0 tag" ,
"status" : "in_progress" ,
"priority" : "urgent" ,
"dueDate" : "2026-08-20T17:00:00.000Z" ,
"tagIds" : [ "f1a2b3c4-..." ]
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
Response (`201` ) is the created task. Setting `parentId` requires the parent to exist and not be deleted (`404 NOT_FOUND` otherwise).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Reorder
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`POST /api/tasks/reorder` with `{ "orderedIds": ["id1", "id2", "id3"], "domain": "<domainId>" }` . `domain` is optional; it resolves from the first task when omitted. Every ID must exist in the workspace and be non-deleted, or the request returns `404` . Tasks are assigned `order` 0..n in transaction. Response:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{ "success" : true , "orderedIds" : [ "id1" , "id2" , "id3" ] }
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Get a single task
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /api/tasks/:id` returns the task enriched with `subtasks` , `tags` (array of `{ id, name, color }` ), `dependencies` (tasks this task blocks on), and `dependents` (tasks that block on this one).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Update
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`PATCH /api/tasks/:id` accepts any subset of the create fields. A task cannot be its own parent, and `parentId` cycles are rejected with `400 VALIDATION_ERROR` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Delete
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`DELETE /api/tasks/:id` soft-deletes the task (sets `deletedAt` ) and returns `204` . Recurring spawns for the task are stopped.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Change status
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`POST /api/tasks/:id/status` with `{ "status": "done" }` . Marking a task `done` also sets `completedAt` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Tags
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`POST /api/tasks/:id/tags` with `{ "tagId": "<uuid>" }` returns `201 { "success": true }` . Re-assigning the same tag is a no-op, not an error. `DELETE /api/tasks/:id/tags/:tagId` returns `204` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Comments and attachments
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Comments and attachment metadata are stored in the activity feed with entity types `comment` and `attachment` . `POST /api/tasks/:id/comments` takes `{ "content": "..." }` and returns `201 { "success": true }` . `GET` endpoints return `{ items, totalItems }` , newest first.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Habits
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Habit frequencies: `daily` , `weekly` , `custom` . Difficulties: `easy` , `medium` , `hard` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/habits` | List habits |
| `POST` | `/api/habits` | Create a habit |
| `GET` | `/api/habits/:id` | Get a habit with recent completions, tags |
| `PATCH` | `/api/habits/:id` | Update a habit |
| `DELETE` | `/api/habits/:id` | Soft-delete a habit |
| `POST` | `/api/habits/:id/complete` | Log a completion for today |
| `GET` | `/api/habits/:id/completions` | Completion history |
| `POST` | `/api/habits/:id/tags` | Assign a tag |
| `DELETE` | `/api/habits/:id/tags/:tagId` | Remove a tag |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### List habits
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:** `domain` , `active` (`true` /`false` ), `frequency` , `difficulty` , `tag` (comma-separated tag IDs), `search` /`filter` (match on name), `sort` (`created` , `updated` , `name` , `frequency` , `difficulty` , `streak_count` ; default `-created` ), `order` , `page` /`perPage` , `limit` /`offset` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Create
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`name` is required. Other fields: `description` , `domain` (defaults to active), `frequency` , `difficulty` , `goalPerPeriod` (default 1), `unit` , `reminderTime` , `skipDays` (array of 0-6 weekday numbers), `moodTracking` , `active` (default true), `tagIds` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Complete
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`POST /api/habits/:id/complete` with:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{ "value" : 1 , "mood" : 5 , "notes" : "Felt great" }
```
`value` is required (default 1). `mood` is 1-5, `notes` optional. Response (`201` ) includes the stored completion plus updated streaks:
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"completion" : {
"id" : "d4e5f6a7-..." ,
"habitId" : "e5f6a7b8-..." ,
"date" : "2026-08-10T12:00:00.000Z" ,
"value" : 1 ,
"mood" : 5 ,
"notes" : "Felt great" ,
"createdAt" : "2026-08-10T12:00:00.000Z"
},
"streakCount" : 3 ,
"bestStreak" : 5
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
### Completions
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /api/habits/:id/completions` supports `from` and `to` (ISO timestamps), `limit` (default 365, max 1000), `offset` , and `order` (`asc` /`desc` ). Returns `{ items, totalItems, limit, offset }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Projects
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Project statuses: `active` , `paused` , `completed` , `archived` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/projects` | List projects |
| `POST` | `/api/projects` | Create a project |
| `GET` | `/api/projects/:id` | Get a project with sections, tasks, progress |
| `PATCH` | `/api/projects/:id` | Update a project |
| `DELETE` | `/api/projects/:id` | Soft-delete a project |
| `GET` | `/api/projects/:id/sections` | List sections |
| `POST` | `/api/projects/:id/sections` | Create a section |
| `GET` | `/api/projects/:id/sections/:sid` | Get a section |
| `PATCH` | `/api/projects/:id/sections/:sid` | Update a section |
| `DELETE` | `/api/projects/:id/sections/:sid` | Delete a section |
| `GET` | `/api/projects/:id/members` | List members (from activity feed) |
| `POST` | `/api/projects/:id/members` | Add a member |
| `DELETE` | `/api/projects/:id/members/:uid` | Remove a member |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### List projects
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:** `domain` , `status` (comma-separated), `search` /`filter` (match on name), `sort` (`created` , `updated` , `name` , `status` , `target_date` ; default `-created` ), `order` , `page` /`perPage` , `limit` /`offset` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Each item carries `tags` , `taskCount` , `completedCount` , and `progress` (0-100).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Create
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`name` is required. Other fields: `description` , `domain` (defaults to active), `status` (default `active` ), `color` , `icon` , `targetDate` , `tagIds` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Sections
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Sections group tasks within a project. `kind` is `section` (default) or `milestone` ; `status` is `planned` , `in_progress` , or `complete` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Create with `{ "name": "Design phase", "kind": "milestone", "status": "in_progress", "targetDate": "...", "sortOrder": 1 }` . `sortOrder` defaults to one past the current max. Sections are hard-deleted (`204` ); they have no `deleted_at` column.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Members
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Members are recorded in the activity feed with entity type `member` . `POST /api/projects/:id/members` takes `{ "userId": "<uuid>", "role": "member" }` (role optional, default `member` ) and returns `201 { "success": true }` . Remove with `DELETE /api/projects/:id/members/:uid` (`204` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Notes
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Notes store Tiptap HTML in `content` . Wikilinks in the content (Obsidian-style links to other notes and entities) are parsed and kept in sync on create and update.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/notes` | List notes |
| `POST` | `/api/notes` | Create a note |
| `GET` | `/api/notes/:id` | Get a note with tags, backlinks, outgoing links |
| `PATCH` | `/api/notes/:id` | Update a note |
| `DELETE` | `/api/notes/:id` | Soft-delete a note |
| `GET` | `/api/notes/:id/backlinks` | Notes that link to this one |
| `GET` | `/api/notes/:id/versions` | Edit history (from activity feed) |
| `POST` | `/api/notes/:id/tags` | Assign a tag |
| `DELETE` | `/api/notes/:id/tags/:tagId` | Remove a tag |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### List notes
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:** `domain` , `pinned` (`true` ), `archived` (`true` /`false` /`all` ; default excludes archived), `tag` (comma-separated tag IDs), `search` /`filter` (match on title), `sort` (`title` , `created_at` , `updated_at` , `is_pinned` ; default `-updated_at` ), `order` , `page` /`perPage` , `limit` /`offset` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Create
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`title` is required. Other fields: `content` (Tiptap HTML), `domain` (defaults to active), `isPinned` , `isArchived` , `tagIds` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Search
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Full-text search across tasks, notes, projects, habits, and domains. Results are scoped to the caller's active domain. Backed by Postgres `search_vector` columns with `ts_headline` snippets.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
#### `GET /api/search`
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:**
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Parameter | Type | Description |
|-----------|------|-------------|
| `q` | string | Search query (required; empty returns `{ results: [], totalCount: 0 }` ) |
| `types` | string | Comma-separated entity types; defaults to all five |
| `limit` | number | Max results (default 20, max 50) |
| `offset` | number | Offset into the ranked results (default 0) |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Entity types: `task` , `note` , `project` , `habit` , `domain` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{
"results" : [
{
"id" : "c3d4e5f6-..." ,
"type" : "task" ,
"title" : "Complete documentation" ,
"snippet" : "Complete <mark>documentation</mark> for the API..." ,
"score" : 0.42 ,
"workspaceId" : "b2c3d4e5-..." ,
"link" : "/tasks/c3d4e5f6-..."
}
],
"totalCount" : 1 ,
"query" : "documentation"
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Two stubs exist: `GET /api/search/recent` returns `{ items: [], totalItems: 0 }` and `POST /api/search/index` returns `{ success: true, message: "Reindex triggered" }` . Neither performs work yet.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Calendar
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/calendar/events` | List events in a time range |
| `POST` | `/api/calendar/events` | Create an event |
| `PATCH` | `/api/calendar/events/:id` | Update an event |
| `DELETE` | `/api/calendar/events/:id` | Delete an event |
| `GET` | `/api/calendar/upcoming` | Events in the next N days |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /events` takes `from` and `to` (ISO timestamps, matched against `startTime` ), plus `domain` . Returns events ordered by `startTime` ascending.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create:**
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"title" : "Sprint review" ,
"description" : "Demo the new dashboard" ,
"startTime" : "2026-08-14T15:00:00.000Z" ,
"endTime" : "2026-08-14T16:00:00.000Z" ,
"allDay" : false ,
"color" : "#3b82f6" ,
"entityType" : "task" ,
"entityId" : "c3d4e5f6-..." ,
"recurrenceRule" : null
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`title` , `startTime` , and `domain` are required. `GET /upcoming?days=7` (default 7, max 365) returns events from now through `days` days ahead. Events are hard-deleted (`204` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Graph
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
The knowledge graph of a domain: nodes for notes, tasks, habits, projects, sections, tags, and the domain itself; edges for note links, note-to-entity links, task dependencies, and domain membership.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/graph/nodes` | All nodes for a domain |
| `GET` | `/api/graph/edges` | All edges for a domain |
| `POST` | `/api/graph/edges` | Create a relationship |
| `DELETE` | `/api/graph/edges/:id` | Delete a relationship |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`domain` is required on both `GET` endpoints; omitting it returns `400 VALIDATION_ERROR` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Node shape: `{ id, label, type, color }` with types `task` , `habit` , `project` , `note` , `section` , `tag` , `domain` . Edge shape: `{ source, target, type }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create an edge** with `{ "sourceId": "<uuid>", "targetId": "<uuid>", "type": "note_link" }` . Supported types:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Type | Meaning |
|------|---------|
| `note_link` | Note to note (source and target are notes) |
| `note_entity` | Note to task (target is a task) |
| `task_dependency` | Task depends on task (source depends on target) |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Both endpoints must belong to the caller's domain, verified before any write. Response (`201` ): `{ "success": true }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Delete an edge** with `DELETE /api/graph/edges/:id` where `:id` is `"<sourceId>-<targetId>"` . The server resolves the source entity type, deletes from `note_links` or `task_dependencies` , and returns `204` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Dashboard
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Per-user dashboard widgets, scoped to a domain.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/dashboard/widgets` | List the caller's widgets for a domain |
| `POST` | `/api/dashboard/widgets` | Add a widget |
| `PATCH` | `/api/dashboard/widgets/:id` | Update a widget |
| `DELETE` | `/api/dashboard/widgets/:id` | Remove a widget |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create:**
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"type" : "task_progress" ,
"title" : "Sprint progress" ,
"config" : { "projectId" : "..." },
"layout" : { "x" : 0 , "y" : 0 , "w" : 4 , "h" : 2 }
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`type` is required. `layout` defaults to `{ x: 0, y: 0, w: 2, h: 2 }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Agents
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Agents are API-authenticated assistants scoped to a domain. Creating one generates an `apiKey` (a UUID) that the agent presents as a bearer token. The MCP endpoint accepts the same key.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Permission tiers: `full_access` , `read_only` , `content_creator` , `task_manager` , `custom` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/agents` | List agents |
| `POST` | `/api/agents` | Create an agent |
| `GET` | `/api/agents/:id` | Get an agent |
| `PATCH` | `/api/agents/:id` | Update an agent |
| `DELETE` | `/api/agents/:id` | Delete an agent |
| `POST` | `/api/agents/:id/permissions` | Set permission tier and custom permissions |
| `GET` | `/api/agents/:id/permissions` | Get permissions |
| `GET` | `/api/agents/activity` | Activity across all agents in a domain |
| `GET` | `/api/agents/:id/activity` | Activity for one agent (`_all` for all) |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### List
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:** `domain` , `q` (match on name), `sort` (`created` , `updated` , `name` ; default `-created` ), `page` , `perPage` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Create
```json
{
"name" : "Code Assistant" ,
"description" : "Reviews code and suggests changes" ,
"status" : "active" ,
"permissionTier" : "read_only" ,
"customPermissions" : [],
"tags" : [ "assistant" ],
"config" : {}
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`name` and `domain` are required. Response (`201` ) includes the generated `apiKey` . Store it; the API returns it only at creation.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Activity
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /api/agents/activity` and `GET /api/agents/:id/activity` accept `action` , `from` , `to` (ISO timestamps; a bare `YYYY-MM-DD` bounds the whole day), `limit` (default 100, max 500), and `domain` . `GET /api/agents/_all/activity` is equivalent to `/api/agents/activity` . Activity rows carry `agentId` , `action` , `entityType` , `entityId` , `details` , `success` , `errorMessage` , `createdAt` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Webhooks
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Webhooks are domain-scoped outgoing notifications. Creating or updating tasks, habits, projects, and notes enqueues deliveries for matching webhooks. A background worker performs the HTTP delivery and records the result in `webhook_deliveries` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/webhooks` | List webhooks |
| `POST` | `/api/webhooks` | Create a webhook |
| `PATCH` | `/api/webhooks/:id` | Update a webhook |
| `DELETE` | `/api/webhooks/:id` | Delete a webhook |
| `POST` | `/api/webhooks/:id/test` | Enqueue a test delivery |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
### Create
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"name" : "Task Notifications" ,
"url" : "https://example.com/hook" ,
"events" : [ "task.created" , "task.updated" , "task.completed" ],
"secret" : "your-webhook-secret" ,
"active" : true ,
"retryCount" : 3
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`name` , `url` , `events` , and `domain` are required. `secret` enables payload signing. Event names follow `<entity>.<action>` with entities `task` , `habit` , `project` , `note` ; the `test` event fires from the test endpoint.
### Delivery
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
The worker POSTs to the webhook URL with:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
- `Content-Type: application/json`
- `X-Event-Type: <event>`
- `X-ProjectE-Signature: <hex HMAC-SHA256>` when a `secret` is set, computed over the raw JSON body
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Payload:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{
"event" : "task.created" ,
"entity_type" : "task" ,
"entity_id" : "c3d4e5f6-..." ,
"data" : { "title" : "Complete documentation" },
"timestamp" : "2026-08-10T10:30:00.000Z" ,
"workspace_id" : "b2c3d4e5-..."
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Deliveries use a 10 second timeout and are recorded in `webhook_deliveries` with status, status code, response body (first 1000 chars), and attempt count.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`POST /api/webhooks/:id/test` returns `{ "success": true, "message": "Test webhook queued" }` immediately; the worker performs the delivery.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Canvas
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Freeform or graph-mode whiteboards. A canvas has cards positioned on a viewport.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/canvas` | List canvases |
| `POST` | `/api/canvas` | Create a canvas |
| `GET` | `/api/canvas/:id` | Get a canvas with cards and connections |
| `PATCH` | `/api/canvas/:id` | Update a canvas |
| `DELETE` | `/api/canvas/:id` | Delete a canvas |
| `POST` | `/api/canvas/:id/cards` | Create one card |
| `PUT` | `/api/canvas/:id/cards` | Bulk-replace all cards (primary save path) |
| `PATCH` | `/api/canvas/cards/:cardId` | Update one card |
| `DELETE` | `/api/canvas/cards/:cardId` | Delete one card |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create:**
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"name" : "Brainstorm" ,
"description" : "Q3 roadmap ideas" ,
"mode" : "freeform" ,
"viewport" : { "x" : 0 , "y" : 0 , "zoom" : 1 },
"tags" : [ "ideas" ],
"background" : null
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`name` and `domain` are required. `GET /api/canvas/:id` returns the canvas plus `cards` (ordered by `zIndex` ) and `connections` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create a card** with `{ "type": "note", "title": "Idea", "content": "…", "x": 0, "y": 0, "width": 200, "height": 150, "zIndex": 1 }` . Defaults: `type` `note` , `width` 200, `height` 150, `zIndex` one past the current max.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Bulk save** `PUT /api/canvas/:id/cards` replaces all cards in one transaction:
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"cards" : [
{ "id" : "optional-existing-id" , "type" : "note" , "content" : "First card" , "x" : 0 , "y" : 0 },
{ "type" : "note" , "content" : "Second card" , "x" : 240 , "y" : 0 }
]
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
Cards without an `id` are created; `zIndex` defaults to the array index. Response is the canvas plus the saved cards. Canvases, cards, and connections are hard-deleted (`204` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Daily Notes
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
One note per calendar day per domain (unique on `date` + `domain_id` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/daily-notes` | Get a day's note or list all for the domain |
| `POST` | `/api/daily-notes` | Create a note for a date |
| `PATCH` | `/api/daily-notes/:id` | Update a note |
| `DELETE` | `/api/daily-notes/:id` | Delete a note |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /api/daily-notes?date=2026-08-10` returns the note object or `null` . Without `date` , it returns `{ items, totalItems }` ordered newest first.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create:**
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"date" : "2026-08-10" ,
"content" : "Deep work on the API docs." ,
"mood" : 8 ,
"energy" : 7 ,
"customFields" : {}
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`date` must be `YYYY-MM-DD` , `mood` and `energy` are 1-10. Daily notes are hard-deleted (`204` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Tags
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Tags are global (no domain column). They attach to tasks, habits, projects, and notes through junction tables.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/tags` | List tags |
| `POST` | `/api/tags` | Create a tag |
| `GET` | `/api/tags/:id` | Get a tag |
| `PATCH` | `/api/tags/:id` | Update a tag |
| `DELETE` | `/api/tags/:id` | Delete a tag |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**List query parameters:** `page` , `perPage` , `sort` (`name` , `created` , `updated` ; default `name` ), `filter` (exact match on scope: `global` , `tasks` , `habits` , `projects` , `notes` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create:**
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{ "name" : "urgent" , "color" : "#ef4444" , "scope" : "global" }
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`name` is required, `scope` defaults to `global` . Tags are hard-deleted (`204` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Custom Fields
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Per-domain field definitions attached to entities.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/custom-fields` | List fields, optionally for one entity type |
| `POST` | `/api/custom-fields` | Create a field |
| `PATCH` | `/api/custom-fields/:id` | Update a field |
| `DELETE` | `/api/custom-fields/:id` | Delete a field |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /api/custom-fields?entity=task` filters by entity type (`task` , `habit` , `project` , `note` , and so on). Results are ordered by `sortOrder` , then `name` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Create:**
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"name" : "Client" ,
"type" : "text" ,
"entityType" : "task" ,
"required" : false ,
"options" : [],
"defaultValue" : null ,
"sortOrder" : 0
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`name` , `entityType` , and `domain` are required. Fields are hard-deleted (`204` ).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Error Log
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Server-side error log, useful for admin screens.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/error-log` | List recent errors |
| `DELETE` | `/api/error-log` | Clear all error logs |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
`GET /api/error-log?level=error&limit=50` filters by level and caps at 200 rows (default 50). Rows carry `level` , `source` , `message` , `stackTrace` , `metadata` , `resolved` , timestamps. `DELETE` returns `{ "deleted": <count> }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Analytics
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
All analytics endpoints accept `range` (days, default 30) and `domain` . Responses include a short `Cache-Control` header. None paginate.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/analytics/productivity` | Task completion rate over the period |
| `GET` | `/api/analytics/habits` | Habit consistency, active streaks, best streak |
| `GET` | `/api/analytics/projects` | Per-project task totals and progress |
| `GET` | `/api/analytics/daily` | Daily task created/completed time series |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Productivity:**
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"taskCompletionRate" : 75 ,
"totalTasks" : 40 ,
"completedTasks" : 30 ,
"period" : 30
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
**Habits:**
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"habitConsistency" : 82 ,
"totalHabits" : 6 ,
"totalLogs" : 148 ,
"activeStreaks" : 5 ,
"bestStreak" : 21 ,
"period" : 30
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
**Projects:**
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{
"projects" : [
{ "id" : "..." , "name" : "Website Redesign" , "totalTasks" : 12 , "completedTasks" : 9 , "progress" : 0.75 }
],
"totalProjects" : 3 ,
"period" : 30
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Daily** returns `{ items: [{ date: "2026-07-12", created: 2, completed: 1 }, ...], period }` with one bucket per day across the range.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Notifications
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
The notification bell feed. Backed by `activity_feed` ; there is no read/unread state yet, so `count` serves as the unread badge. `graph_edge` rows are excluded.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
#### `GET /api/notifications`
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:** `workspace_id` (defaults to active domain), `limit` (default 20, max 100).
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Returns activity from the last 7 days, newest first:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{
"items" : [
{
"id" : "..." ,
"actor" : "User Name" ,
"action" : "completed" ,
"entityType" : "task" ,
"entityId" : "c3d4e5f6-..." ,
"changes" : { "previousStatus" : "in_progress" , "newStatus" : "done" },
"workspaceId" : "b2c3d4e5-..." ,
"createdAt" : "2026-08-10T12:00:00.000Z"
}
],
"count" : 3
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## Export and Import
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
JSON backup and restore, scoped to one domain.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
#### `GET /api/export`
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Lists the exportable collections without exporting anything:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{
"collections" : [
{ "name" : "tasks" , "label" : "Tasks" },
{ "name" : "habits" , "label" : "Habits" },
{ "name" : "projects" , "label" : "Projects" },
{ "name" : "notes" , "label" : "Notes" },
{ "name" : "tags" , "label" : "Tags" },
{ "name" : "agents" , "label" : "Agents" },
{ "name" : "webhooks" , "label" : "Webhooks" }
]
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
#### `POST /api/export`
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Body `{ "collections": ["tasks", "notes"], "domain": "<domainId>" }` . `collections` defaults to all seven; `domain` defaults to the active domain. Returns a JSON dump:
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"version" : "1.0" ,
"exportedAt" : "2026-08-10T10:30:00.000Z" ,
"tasks" : [],
"notes" : []
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
Tags are exported only when used by the domain's entities. Soft-deleted rows are excluded. A failed collection exports as an empty array rather than failing the whole request.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
#### `POST /api/import`
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Body `{ "version": "1.0", "domain": "<domainId>", "tasks": [...], "habits": [...], ... }` . `version` is required. The target domain comes from `body.domain` or `body.domain_id` (or the active domain) and every imported entity is forced into it; payload-supplied `domain_id` values on individual rows are ignored.
2026-07-16 06:19:58 -04:00
```json
{
2026-08-10 08:53:18 +00:00
"success" : true ,
"imported" : 12 ,
"failed" : 0 ,
"results" : [
{ "collection" : "tasks" , "imported" : 10 , "failed" : 0 , "errors" : [] },
{ "collection" : "notes" , "imported" : 2 , "failed" : 0 , "errors" : [] }
]
2026-07-16 06:19:58 -04:00
}
```
2026-08-10 08:53:18 +00:00
`success` is true when nothing failed. Per-collection `errors` hold up to 5 messages.
## Realtime (SSE)
2026-07-16 06:19:58 -04:00
#### `GET /api/realtime`
2026-08-10 08:53:18 +00:00
Server-sent events stream backed by PostgreSQL `LISTEN/NOTIFY` on the `project_e_events` channel. Requires authentication.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Query parameters:**
2026-07-16 06:19:58 -04:00
| Parameter | Type | Description |
|-----------|------|-------------|
2026-08-10 08:53:18 +00:00
| `workspace_id` | string | Only forward events for this workspace. When omitted, all workspaces' events are forwarded. |
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
The ownership check is an IDOR guard: when `workspace_id` is supplied, the caller must own that workspace, or the request is rejected before the stream opens.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
On connect the server sends a `connected` event, then forwards activity events:
2026-07-16 06:19:58 -04:00
```
2026-08-10 08:53:18 +00:00
data: {"type":"connected","workspace_id":"b2c3d4e5-..."}
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
data: {"type":"task","action":"created","id":"c3d4e5f6-...","workspace_id":"b2c3d4e5-..."}
2026-07-16 06:19:58 -04:00
```
2026-08-10 08:53:18 +00:00
A `:ping` comment arrives every 30 seconds to keep the connection alive. Events are plain `data:` frames, not named events. Unauthenticated requests get `401` with `{ "error": "Unauthorized" }` .
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
## MCP
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Model Context Protocol over JSON-RPC 2.0. `POST /api/mcp` is a stateless HTTP endpoint; there are no sessions to establish. Requires an API key in the `Authorization: Bearer <apiKey>` header. The key is validated against the `api_keys` table (SHA-256 hash comparison). JWT and cookie auth are not accepted here.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Requests and responses use the JSON-RPC 2.0 envelope:
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
```json
{
"jsonrpc" : "2.0" ,
"method" : "tools/call" ,
2026-09-07 20:19:37 +00:00
"params" : { "name" : "tasks.list" , "arguments" : { "domain_id" : "b2c3d4e5-..." , "state_group" : "unstarted" } },
2026-08-10 08:53:18 +00:00
"id" : 1
}
2026-07-16 06:19:58 -04:00
```
2026-08-10 08:53:18 +00:00
```json
{
"jsonrpc" : "2.0" ,
"result" : { "content" : [{ "type" : "text" , "text" : "{\"items\":[...],\"total\":3}" }] },
"id" : 1
}
```
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
Tool results are JSON strings inside `content[0].text` . Tool calls that pass `domain_id` or `workspace_id` are ownership-checked before execution.
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
**Methods:**
2026-07-16 06:19:58 -04:00
2026-08-10 08:53:18 +00:00
| Method | Purpose |
|--------|---------|
| `initialize` | Negotiate protocol version `2024-11-05` , server `project-e` v1.0.0 |
| `tools/list` | List available tools and schemas |
| `tools/call` | Invoke a tool |
| `resources/list` | List resource URIs |
| `resources/read` | Read a resource URI (stub) |
| `server/discover` | Legacy discovery: server info plus all tools |
**Tools:**
| Tool | Description |
|------|-------------|
| `tasks.list` | List tasks with optional filters |
| `tasks.create` | Create a task |
| `tasks.update` | Update a task |
| `tasks.delete` | Delete a task |
| `tasks.complete` | Mark a task done |
| `habits.list` | List habits |
| `habits.create` | Create a habit |
| `habits.complete` | Log a habit completion |
| `projects.list` | List projects |
| `projects.create` | Create a project |
| `notes.list` | List notes |
| `notes.create` | Create a note |
| `notes.update` | Update a note |
| `notes.search` | Search notes |
| `domains.list` | List domains |
| `domains.create` | Create a domain |
| `search.query` | Cross-entity search |
| `activity.list` | List activity feed entries |
**Errors** use JSON-RPC codes:
| Code | Meaning |
|------|---------|
| `-32700` | Parse error |
| `-32600` | Invalid request |
| `-32601` | Method or tool not found |
| `-32602` | Invalid params (including missing required args) |
| `-32603` | Internal error |
| `-32001` | Unauthorized (missing or invalid API key, HTTP 401) |
`GET /api/mcp` returns HTTP 405; the endpoint only accepts POST.
2026-07-16 06:19:58 -04:00
## Rate Limiting
2026-08-10 08:53:18 +00:00
The API does not enforce rate limiting at the application level. Configure limits at the reverse proxy or infrastructure layer.