Skip to main content

Morphee API Reference

Complete API documentation for Morphee backend.


Base URL

Development: http://localhost:8000 Production: https://yourdomain.com/api


API Overview

The Morphee API is built with FastAPI and provides:

  • RESTful endpoints for group, space, task, and integration management
  • WebSocket support for real-time updates
  • Async/await for all operations
  • Auto-generated OpenAPI documentation at /docs
  • JSON request/response format

Authentication

Status: Implemented (Supabase Auth with GoTrue)

Protected endpoints require JWT authentication in the Authorization header:

Authorization: Bearer <jwt_token>

Get your access token by signing in or signing up through the /api/auth/signup or /api/auth/signin endpoints.

Authentication Flow:

  1. Sign up/api/auth/signup → Receive access_token and refresh_token
  2. Sign in/api/auth/signin → Receive access_token and refresh_token
  3. Use access_token in Authorization: Bearer <token> header for protected endpoints
  4. Refresh token when expired → /api/auth/refresh

Common Response Formats

Success Response

{
"id": "uuid",
"status": "success",
"data": { ... }
}

Error Response

{
"detail": "Error message",
"status_code": 400,
"error_type": "ValidationError"
}

HTTP Status Codes

  • 200 OK: Successful GET request
  • 201 Created: Successful POST request
  • 204 No Content: Successful DELETE request
  • 400 Bad Request: Invalid request data
  • 404 Not Found: Resource not found
  • 422 Unprocessable Entity: Validation error
  • 500 Internal Server Error: Server error

Health & Status

Check System Health

GET /health

Returns the health status of the backend service.

Response

{
"status": "healthy",
"service": "morphee-backend",
"version": "2.0.0"
}

Status Codes

  • 200 OK: Service is healthy
  • 503 Service Unavailable: Service is unhealthy

Deep Health Check

GET /health/deep

Performs a deep health check by verifying connectivity to critical dependencies (PostgreSQL database and Redis).

Response (200 OK)

{
"status": "healthy",
"service": "morphee-backend",
"version": "2.0.0",
"checks": {
"database": true,
"redis": true
}
}

Response (503 Service Unavailable - degraded state)

{
"status": "degraded",
"service": "morphee-backend",
"version": "2.0.0",
"checks": {
"database": true,
"redis": false
}
}

Check Status Values

  • true: Service is healthy and reachable
  • false: Service is unhealthy or unreachable
  • null: Service is not configured (e.g., Redis not enabled)

Status Codes

  • 200 OK: All critical services are healthy
  • 503 Service Unavailable: One or more critical services are unhealthy

Example

curl http://localhost:8000/health/deep

Root Endpoint

GET /

Returns basic service information.

Response

{
"service": "Morphee Backend",
"version": "2.0.0",
"status": "running"
}

Authentication API

Sign Up

POST /api/auth/signup

Create a new user account with Supabase Auth.

Request Body

{
"email": "user@example.com",
"password": "securepassword123",
"name": "John Doe",
"birthdate": "2010-03-15",
"parent_email": "parent@example.com",
"country_code": "FR"
}

birthdate, parent_email, and country_code are optional. If birthdate indicates the user is a minor (age < regional threshold: 16 for EU, 13 for US), parent_email becomes required. A parental consent email is sent and the minor cannot log in until the parent verifies consent.

Response (201 Created)

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "v1.MRjy8FIV...",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"expires_at": "2026-02-10T12:00:00Z",
"parental_consent_pending": false
}

If parental_consent_pending is true, the frontend should redirect to /auth/parent-consent-pending.

Status Codes

  • 201 Created: User created successfully
  • 400 Bad Request: User already exists, invalid data, or minor without parent email
  • 422 Unprocessable Entity: Validation error

Sign In

POST /api/auth/signin

Authenticate an existing user.

Request Body

{
"email": "user@example.com",
"password": "securepassword123"
}

Response (200 OK)

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "v1.MRjy8FIV...",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"expires_at": "2026-02-10T12:00:00Z"
}

Status Codes

  • 200 OK: Authentication successful
  • 401 Unauthorized: Invalid credentials
  • 422 Unprocessable Entity: Validation error

Refresh Token

POST /api/auth/refresh

Refresh an expired access token using a refresh token.

Request Body

{
"refresh_token": "v1.MRjy8FIV..."
}

Response (200 OK)

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "v1.NewRefreshToken...",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"expires_at": "2026-02-10T13:00:00Z"
}

Status Codes

  • 200 OK: Token refreshed successfully
  • 401 Unauthorized: Invalid or expired refresh token

Get Current User

GET /api/auth/me

Get information about the currently authenticated user.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"role": "authenticated",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"morphee_user_id": "770e8400-e29b-41d4-a716-446655440002"
}

**Status Codes**

- `200 OK`: User info retrieved
- `401 Unauthorized`: Invalid or missing token
- `403 Forbidden`: No authorization header

---

### Sign Out

```http
POST /api/auth/signout

Sign out the current user and invalidate their session.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"status": "signed_out",
"message": "Successfully signed out"
}

Status Codes

  • 200 OK: Sign out successful
  • 401 Unauthorized: Invalid or missing token

Get User Profile

GET /api/auth/profile

Get the current user's profile information (name, email, avatar).

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe",
"avatar_url": "https://example.com/avatar.jpg",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"created_at": "2026-02-10T10:00:00Z"
}

Status Codes

  • 200 OK: Profile retrieved
  • 401 Unauthorized: Invalid or missing token

Update User Profile

PATCH /api/auth/profile

Update the current user's profile information.

Headers

Authorization: Bearer <access_token>

Request Body

{
"name": "Jane Doe",
"avatar_url": "https://example.com/new-avatar.jpg"
}

Updatable Fields

  • name (string): Display name
  • avatar_url (string): URL to avatar image

Response (200 OK)

Returns the updated user profile object.

Status Codes

  • 200 OK: Profile updated
  • 401 Unauthorized: Invalid or missing token
  • 422 Unprocessable Entity: Validation error

Change Password

POST /api/auth/change-password

Change the current user's password.

Headers

Authorization: Bearer <access_token>

Request Body

{
"current_password": "oldpassword123",
"new_password": "newpassword456"
}

Required Fields

  • current_password (string): The user's current password
  • new_password (string): The new password (minimum 6 characters)

Response (200 OK)

{
"status": "password_changed",
"message": "Password changed successfully"
}

Status Codes

  • 200 OK: Password changed
  • 400 Bad Request: Current password is incorrect
  • 401 Unauthorized: Invalid or missing token
  • 422 Unprocessable Entity: New password too short or validation error

Export User Data

GET /api/auth/export?format=json|markdown

Export all user data in compliance with GDPR Article 15 (Right of Access) and Article 20 (Right to Data Portability).

Headers

Authorization: Bearer <access_token>

Query Parameters

  • format (string, optional): Export format. Options:
    • json (default): Single JSON file with all data
    • markdown: ZIP archive with human-readable Markdown files

Response (200 OK)

JSON format:

{
"export_date": "2026-02-13T12:00:00Z",
"user": {
"id": "uuid",
"name": "John Doe",
"email": "john@example.com",
"role": "parent",
"group_id": "uuid",
"created_at": "2026-01-01T00:00:00Z"
},
"group": {
"id": "uuid",
"name": "The Doe Family",
"timezone": "America/New_York",
"role": "parent"
},
"conversations": [...],
"tasks": [...],
"spaces": [...],
"memories": [...],
"schedules": [...],
"notifications": [...],
"skills": [...],
"oauth_connections": [...],
"consents": [...]
}

Markdown format:

  • ZIP archive containing:
    • README.md — Export metadata
    • profile.json — User profile
    • group.json — Group membership
    • conversations/*.md — Human-readable conversation files
    • tasks.json, spaces.json, memories.json, etc.

Status Codes

  • 200 OK: Export generated successfully
  • 400 Bad Request: Invalid format parameter
  • 401 Unauthorized: Invalid or missing token

Example

curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8000/api/auth/export?format=markdown" \
-o morphee-export.zip

Delete Account

POST /api/auth/delete-account

Permanently delete the current user's account and all associated data (GDPR Article 17 — Right to Erasure).

Headers

Authorization: Bearer <access_token>

Request Body

{
"confirmation": "DELETE"
}

Required Fields

  • confirmation (string): Must be exactly "DELETE" to confirm

Response (200 OK)

{
"status": "deleted",
"message": "Account and all associated data deleted"
}

Status Codes

  • 200 OK: Account deleted
  • 400 Bad Request: Confirmation text doesn't match or deletion failed
  • 401 Unauthorized: Invalid or missing token

List Consents

GET /api/auth/consents

Get all consent records for the current user.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

[
{
"consent_type": "llm_data_sharing",
"granted_at": "2026-02-14T12:00:00Z"
},
{
"consent_type": "memory_extraction",
"granted_at": "2026-02-14T12:05:00Z"
}
]

POST /api/auth/consents

Grant consent for a specific data processing purpose.

Headers

Authorization: Bearer <access_token>

Request Body

{
"consent_type": "llm_data_sharing"
}

Valid consent types: llm_data_sharing, memory_extraction, google_calendar, google_gmail, push_notifications

Response (200 OK)

{
"status": "granted",
"consent_type": "llm_data_sharing"
}

DELETE /api/auth/consents/{consent_type}

Withdraw a previously granted consent.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"status": "withdrawn",
"consent_type": "llm_data_sharing"
}

Check Required Consents

GET /api/auth/consents/required

Check which required consents are still missing for the current user.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"missing_consents": ["llm_data_sharing"],
"all_granted": false
}

List SSO Providers

GET /api/auth/providers

Discover which social/SSO login providers are enabled on the GoTrue instance. Filters to supported providers only (google, apple, azure, github, discord, slack_oidc).

Response (200 OK)

{
"providers": ["google", "apple"]
}

Status Codes

  • 200 OK: Providers retrieved (may be empty array)
  • 502 Bad Gateway: GoTrue authentication service unreachable

SSO Authorize

GET /api/auth/sso/{provider}

Get the GoTrue authorization URL for a social login provider. The frontend redirects the user to this URL to begin the OAuth flow.

Path Parameters

  • provider (required): Provider name (google, apple, azure, github, discord, slack_oidc)

Query Parameters

  • redirect_to (optional): Custom redirect URL after OAuth. Default: http://localhost:5173/auth/callback. For Tauri desktop: http://tauri.localhost/auth/callback. For mobile: morphee://auth/callback. Allowed schemes: http, https, morphee.

Response (200 OK)

{
"authorize_url": "http://localhost:9999/authorize?provider=google&redirect_to=http://localhost:5173/auth/callback"
}

Status Codes

  • 200 OK: Authorization URL generated
  • 400 Bad Request: Unsupported provider

SSO Callback

POST /api/auth/sso/callback

Complete SSO login by verifying the tokens from GoTrue and creating a Morphee user if needed. This is the "find or create" endpoint — if the SSO user doesn't have a public.users row, one is created with group_id=null (triggers onboarding).

Request Body

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "v1.MRjy8FIV..."
}

Response (200 OK)

{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "v1.MRjy8FIV...",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@gmail.com",
"expires_at": 1700000000
}

Status Codes

  • 200 OK: SSO login successful (existing or new user)
  • 401 Unauthorized: Invalid SSO token

SSO Login Flow

  1. Frontend calls GET /api/auth/providers to discover enabled providers
  2. User clicks "Sign in with Google" → frontend calls GET /api/auth/sso/google
  3. Frontend redirects to authorize_url (full-page redirect)
  4. User authenticates with provider → GoTrue redirects to /auth/callback#access_token=...&refresh_token=...
  5. Frontend extracts tokens from URL fragment → calls POST /api/auth/sso/callback
  6. Backend verifies token, finds or creates morphee_user, returns AuthResponse
  7. Frontend saves auth, fetches /api/auth/me, redirects to / or /onboarding

GET /api/auth/parent-consent/verify/{token}

Public endpoint (no auth required). Called when a parent clicks the consent email link.

Response (200 OK)

{
"message": "Parental consent verified successfully",
"child_email": "kid@example.com"
}

Status Codes

  • 200 OK: Consent verified
  • 400 Bad Request: Invalid or expired token

POST /api/auth/parent-consent/resend

Authenticated endpoint for minors to resend the consent email to their parent.

Headers

Authorization: Bearer <access_token>

Status Codes

  • 200 OK: Email resent
  • 400 Bad Request: User is not a minor or no parent email on file
  • 401 Unauthorized: Not authenticated

GET /api/auth/parent-consent/status

Authenticated endpoint to check if parental consent has been verified.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"is_minor": true,
"parent_consent_status": "pending",
"parent_email": "parent@example.com"
}

Status Codes

  • 200 OK: Status returned
  • 401 Unauthorized: Not authenticated

Group Management API

Create Group

POST /api/groups/

Create a new group. The creator becomes an admin of the group.

Headers

Authorization: Bearer <access_token>

Request Body

{
"name": "Smith Family",
"timezone": "America/New_York"
}

Response (201 Created)

{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Smith Family",
"timezone": "America/New_York",
"created_at": "2026-02-10T10:00:00Z",
"updated_at": "2026-02-10T10:00:00Z"
}

Status Codes

  • 201 Created: Group created successfully
  • 400 Bad Request: User already in a group or invalid data
  • 401 Unauthorized: Not authenticated

Get My Group

GET /api/groups/me

Get the current user's group information.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Smith Family",
"timezone": "America/New_York",
"created_at": "2026-02-10T10:00:00Z",
"updated_at": "2026-02-10T10:00:00Z"
}

Status Codes

  • 200 OK: Group retrieved
  • 403 Forbidden: User not in a group
  • 404 Not Found: Group not found
  • 401 Unauthorized: Not authenticated

Update My Group

PATCH /api/groups/me

Update the current user's group information. Only admins can update.

Headers

Authorization: Bearer <access_token>

Request Body

{
"name": "Updated Group Name",
"timezone": "Europe/London"
}

Response (200 OK)

{
"id": "660e8400-e29b-41d4-a716-446655440001",
"name": "Updated Group Name",
"timezone": "Europe/London",
"created_at": "2026-02-10T10:00:00Z",
"updated_at": "2026-02-10T11:30:00Z"
}

Status Codes

  • 200 OK: Group updated
  • 400 Bad Request: Invalid data
  • 403 Forbidden: Not an admin or not in group
  • 401 Unauthorized: Not authenticated

Delete My Group

DELETE /api/groups/me

Delete the current user's group. Removes all members. Only admins can delete.

Headers

Authorization: Bearer <access_token>

Response (200 OK)

{
"status": "deleted",
"message": "Group deleted successfully"
}

Status Codes

  • 200 OK: Group deleted
  • 403 Forbidden: Not an admin or not in group
  • 404 Not Found: Group not found
  • 401 Unauthorized: Not authenticated

Group Members & Invites API

Authentication Required: All member/invite endpoints require authentication via JWT token.

Group Isolation: Members and invites are scoped to the authenticated user's group.

Create Invite

POST /api/groups/me/invites

Create a group invite. Only users with "parent" role can send invites. The invite generates a unique token valid for 7 days.

Request Body

{
"email": "newmember@example.com",
"role": "member"
}

Required Fields

  • email (string, 5-255 chars): Email address to invite
  • role (string): Role for the invitee — "parent", "child", or "member" (default: "member")

Response (201 Created)

{
"id": "uuid",
"group_id": "uuid",
"email": "newmember@example.com",
"role": "member",
"invited_by": "uuid",
"status": "pending",
"expires_at": "2026-02-20T10:00:00Z",
"created_at": "2026-02-13T10:00:00Z"
}

Status Codes

  • 201 Created: Invite created
  • 400 Bad Request: Active invite exists, user already a member, or not a parent
  • 401 Unauthorized: Missing or invalid token

List Invites

GET /api/groups/me/invites

List pending invites for the current user's group.

Response (200 OK): Array of invite objects filtered to status: "pending".


Cancel Invite

DELETE /api/groups/me/invites/{invite_id}

Cancel a pending invite. Only parents can cancel.

Status Codes

  • 200 OK: Invite cancelled
  • 403 Forbidden: Not a parent
  • 404 Not Found: Invite not found or already processed

POST /api/groups/me/invites/link

Create a shareable invite link without requiring the invitee's email — useful for children without an email address. Returns a token that can be shared via deep link or QR code.

Request Body: { "role": "parent | child | member" } (default: child)

Response (201 Created): Invite object with email: null and a token field.


Accept Invite

POST /api/groups/invites/accept

Accept a group invite using the invite token. Does NOT require group membership. Sets the user's group_id and role.

Request Body

{
"token": "invite-token-string"
}

Status Codes

  • 200 OK: Invite accepted, user added to group
  • 400 Bad Request: Invite not found, expired, already processed, or user already in a group

Decline Invite

POST /api/groups/invites/decline

Decline a group invite. Does not require authentication.

Request Body

{
"token": "invite-token-string"
}

List Members

GET /api/groups/me/members

List all members of the current user's group.

Response (200 OK)

[
{
"id": "uuid",
"name": "John Doe",
"email": "john@example.com",
"role": "parent",
"avatar_url": null,
"created_at": "2026-02-10T10:00:00Z"
}
]

Update Member Role

PATCH /api/groups/me/members/{user_id}

Update a member's role. Only parents can change roles. Cannot demote the last parent.

Request Body

{
"role": "child"
}

Valid Roles: "parent", "child", "member"


Remove Member

DELETE /api/groups/me/members/{user_id}

Remove a member from the group. Only parents can remove members. Cannot remove the last parent.

Status Codes

  • 200 OK: Member removed
  • 400 Bad Request: Not a parent or cannot remove last parent
  • 404 Not Found: Member not found

Space API

Authentication Required: All Space endpoints require authentication via JWT token.

Group Isolation: Spaces are automatically filtered by the authenticated user's group. Users can only access Spaces belonging to their group.

Create Space

POST /api/spaces/

Create a new Space for the authenticated user's group. The group_id is automatically set from the authenticated user.

Headers

Authorization: Bearer <access_token>

Request Body

{
"name": "My Space",
"description": "Optional space description"
}

Required Fields

  • name (string, 1-200 chars): Space name

Optional Fields

  • description (string, max 2000 chars): Space description
  • parent_space_id (UUID): Parent Space ID for creating sub-Spaces. When set, the new Space is created as a child of the specified parent and inherits its Interfaces.

Response (201 Created)

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"parent_space_id": null,
"name": "My Space",
"description": "Optional space description",
"knowledge_base_path": null,
"created_at": "2026-02-10T10:00:00Z",
"updated_at": "2026-02-10T10:00:00Z"
}

**Status Codes**

- `201 Created`: Space created successfully
- `400 Bad Request`: Invalid request data
- `401 Unauthorized`: Missing or invalid token
- `403 Forbidden`: User not in a group

---

### List Spaces

```http
GET /api/spaces/

Retrieve all Spaces for the authenticated user's group.

Headers

Authorization: Bearer <access_token>

Query Parameters

  • limit (optional): Maximum number of spaces to return (default: 50, min: 1, max: 100)

Response (200 OK)

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"parent_space_id": null,
"name": "My Space",
"description": "Space description",
"knowledge_base_path": null,
"created_at": "2026-02-10T10:00:00Z",
"updated_at": "2026-02-10T10:00:00Z"
}
]

Status Codes

  • 200 OK: Spaces retrieved successfully
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group

Get Space by ID

GET /api/spaces/{space_id}

Retrieve details of a specific Space. Only accessible if the Space belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • space_id (required): UUID of the Space

Response (200 OK)

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"parent_space_id": null,
"name": "My Space",
"description": "Space description",
"knowledge_base_path": null,
"created_at": "2026-02-10T10:00:00Z",
"updated_at": "2026-02-10T10:00:00Z"
}

Status Codes

  • 200 OK: Space retrieved successfully
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Space does not exist or doesn't belong to user's group

Update Space

PATCH /api/spaces/{space_id}

Update an existing Space. Only accessible if the Space belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • space_id (required): UUID of the Space

Request Body

{
"name": "Updated Name",
"description": "Updated description"
}

Updatable Fields

  • name (string, 1-200 chars): Space name
  • description (string, max 2000 chars): Space description

Response (200 OK)

Returns the updated Space object.

Status Codes

  • 200 OK: Space updated successfully
  • 400 Bad Request: Invalid request data
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Space does not exist or doesn't belong to user's group

Delete Space

DELETE /api/spaces/{space_id}

Delete a Space permanently. Tasks belonging to this Space are also affected. Only accessible if the Space belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • space_id (required): UUID of the Space

Response (200 OK)

{
"status": "deleted",
"message": "Space deleted successfully"
}

Status Codes

  • 200 OK: Space deleted successfully
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Space does not exist or doesn't belong to user's group

Register OpenMorph Directory

POST /api/spaces/morph/register

Associate a local .morph/ directory with a Space (V1.0 OpenMorph). Called by the Tauri frontend after morph_init succeeds on the client.

Request Body:

{
"space_id": "uuid",
"morph_path": "string (3-4096 chars)"
}

Response (201 Created): Space object with morph_path set.


Update Morph Path

PATCH /api/spaces/{space_id}/morph-path

Update the .morph/ path for a Space (e.g., after the user moves the directory).

Request Body: { "morph_path": "string (3-4096 chars)" }

Response (200 OK): Updated Space object.


Get Sync Status

GET /api/spaces/{space_id}/sync-status

Return the OpenMorph sync status for a Space.

Response (200 OK):

{
"space_id": "uuid",
"morph_path": "string | null",
"migrated_at": "ISO8601 | null",
"sync_mode": "local-only | morphee-hosted | git-remote",
"has_morph_dir": true
}

Task API

Authentication Required: All task endpoints require authentication via JWT token.

Group Isolation: Tasks are automatically filtered by the authenticated user's group. Users can only access tasks belonging to their group.

List All Tasks

GET /api/tasks/

Retrieve a list of tasks for the authenticated user's group.

Headers

Authorization: Bearer <access_token>

Query Parameters

  • space_id (optional): Filter by Space UUID
  • status (optional): Filter by task status (pending, running, completed, failed, cancelled)
  • limit (optional): Maximum number of tasks to return (default: 50, max: 100)

Response

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Process user message",
"status": "completed",
"metadata": {
"user_id": "user123",
"priority": "high"
},
"created_at": "2026-02-09T12:00:00Z",
"updated_at": "2026-02-09T12:05:00Z"
}
]

Status Codes

  • 200 OK: Tasks retrieved successfully
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group

Example

curl http://localhost:8000/api/tasks/?status=pending&limit=10 \
-H "Authorization: Bearer <access_token>"

Get Task by ID

GET /api/tasks/{task_id}

Retrieve details of a specific task. Only accessible if the task belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • task_id (required): UUID of the task

Response

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Process user message",
"status": "completed",
"metadata": {
"result": "Message processed successfully"
},
"created_at": "2026-02-09T12:00:00Z",
"updated_at": "2026-02-09T12:05:00Z"
}

Status Codes

  • 200 OK: Task retrieved successfully
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Task does not exist or doesn't belong to user's group

Create New Task

POST /api/tasks/

Create a new task for the authenticated user's group. The group_id is automatically set from the authenticated user.

Headers

Authorization: Bearer <access_token>

Request Body

{
"space_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "message",
"description": "Send welcome email",
"input": {
"email": "user@example.com",
"template": "welcome"
}
}

Required Fields

  • space_id (UUID): Space this task belongs to
  • type (string): Task type identifier
  • description (string): Description of the task
  • input (object): Task input data

Optional Fields

  • user_id (UUID): Specific user assignment (defaults to null)

Response (201 Created)

{
"id": "650e8400-e29b-41d4-a716-446655440000",
"space_id": "550e8400-e29b-41d4-a716-446655440000",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"type": "message",
"description": "Send welcome email",
"status": "pending",
"input": {
"email": "user@example.com",
"template": "welcome"
},
"output": null,
"created_at": "2026-02-09T12:00:00Z",
"updated_at": "2026-02-09T12:00:00Z"
}

**Status Codes**

- `201 Created`: Task created successfully
- `400 Bad Request`: Invalid request data
- `401 Unauthorized`: Missing or invalid token
- `403 Forbidden`: User not in a group
- `422 Unprocessable Entity`: Validation error

---

### Update Task

```http
PATCH /api/tasks/{task_id}

Update an existing task. Only accessible if the task belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • task_id (required): UUID of the task

Request Body

{
"status": "running",
"metadata": {
"progress": 50
}
}

Updatable Fields

  • description (string): Task description
  • status (string): Task status (pending, running, completed, failed, cancelled)
  • output (object): Task output data

Response

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Send welcome email",
"status": "running",
"metadata": {
"email": "user@example.com",
"progress": 50
},
"created_at": "2026-02-09T12:00:00Z",
"updated_at": "2026-02-09T12:02:00Z"
}

Status Codes

  • 200 OK: Task updated successfully
  • 400 Bad Request: Invalid request data
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Task does not exist or doesn't belong to user's group

Delete Task

DELETE /api/tasks/{task_id}

Delete a task permanently. Only accessible if the task belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • task_id (required): UUID of the task

Response (200 OK)

{
"status": "deleted",
"message": "Task deleted successfully"
}

Status Codes

  • 200 OK: Task deleted successfully
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Task does not exist or doesn't belong to user's group

Pause Task

POST /api/tasks/{task_id}/pause

Pause a running task. Changes the task status to paused. Only accessible if the task belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • task_id (required): UUID of the task to pause

Response

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Long-running task",
"status": "paused",
"created_at": "2026-02-09T12:00:00Z",
"updated_at": "2026-02-09T12:05:00Z"
}

Status Codes

  • 200 OK: Task paused successfully
  • 400 Bad Request: Task cannot be paused (invalid state)
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Task does not exist or doesn't belong to user's group

Events

  • Emits: task.status_changed

Resume Task

POST /api/tasks/{task_id}/resume

Resume a paused task. Changes the task status to pending so it can be picked up by the task runner. Only accessible if the task belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • task_id (required): UUID of the task to resume

Response

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Long-running task",
"status": "pending",
"created_at": "2026-02-09T12:00:00Z",
"updated_at": "2026-02-09T12:10:00Z"
}

Status Codes

  • 200 OK: Task resumed successfully
  • 400 Bad Request: Task cannot be resumed (invalid state)
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Task does not exist or doesn't belong to user's group

Events

  • Emits: task.status_changed

Cancel Task

POST /api/tasks/{task_id}/cancel

Cancel a task. Changes the task status to cancelled. The task will not be executed. Only accessible if the task belongs to the user's group.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • task_id (required): UUID of the task to cancel

Response

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Scheduled task",
"status": "cancelled",
"created_at": "2026-02-09T12:00:00Z",
"updated_at": "2026-02-09T12:03:00Z"
}

Status Codes

  • 200 OK: Task cancelled successfully
  • 400 Bad Request: Task cannot be cancelled (invalid state)
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Task does not exist or doesn't belong to user's group

Events

  • Emits: task.status_changed

Integration API

The Integration API provides access to the MCP-style integration system for executing actions.

List All Integrations

GET /api/interfaces/

Get a list of all registered integrations. Returns full InterfaceDefinition objects.

Response

[
{
"name": "echo",
"description": "Echo integration for testing",
"version": "1.0.0",
"actions": [
{
"name": "echo",
"description": "Echo back a message",
"parameters": [...],
"ai_access": "execute",
"side_effect": "read",
"returns": {},
"idempotent": true,
"timeout_seconds": 300
}
],
"events": []
},
{
"name": "webhook",
"description": "Generic webhook handler",
"version": "1.0.0",
"actions": [...],
"events": []
}
]

List All Actions

GET /api/interfaces/actions

Get all actions across all registered integrations (flat list).

Response

[
{
"interface": "echo",
"action": "echo",
"description": "Echo back a message",
"ai_access": "execute"
},
{
"interface": "echo",
"action": "delay_echo",
"description": "Echo with a delay",
"ai_access": "execute"
},
{
"interface": "webhook",
"action": "receive",
"description": "Receive and process a webhook payload",
"ai_access": "execute"
},
{
"interface": "webhook",
"action": "send",
"description": "Send a webhook to an external URL",
"ai_access": "propose"
}
]

List Interface Configs

GET /api/interfaces/configs

Authentication Required: Yes (group access)

Get all configured interface settings for the authenticated user's group. Secret values are redacted to "***".

Response

[
{
"id": "uuid",
"group_id": "uuid",
"space_id": null,
"integration_name": "llm",
"config": { "api_key": "***", "model": "claude-sonnet", "max_tokens": 4096 },
"enabled": true,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
]

Get Interface Config

GET /api/interfaces/{interface_name}/config

Authentication Required: Yes (group access)

Get the current configuration for a specific interface. Secret values are redacted.

Path Parameters

  • interface_name (required): Name of the integration

Error Responses

  • 404 Not Found: Interface not found or no config exists

Save Interface Config

PUT /api/interfaces/{interface_name}/config

Authentication Required: Yes (group access)

Create or update the configuration for an interface. Secret fields (per config_schema) are stored in the vault; public fields are stored directly in the database.

Path Parameters

  • interface_name (required): Name of the integration

Request Body

{
"config": { "api_key": "sk-ant-...", "model": "claude-sonnet", "max_tokens": 4096 },
"space_id": null,
"enabled": true
}

Response: InterfaceConfigResponse (secrets redacted)

Error Responses

  • 404 Not Found: Interface not found
  • 400 Bad Request: Interface is not configurable (no config_schema)

Delete Interface Config

DELETE /api/interfaces/{interface_name}/config

Authentication Required: Yes (group access)

Remove the configuration for an interface. Also deletes associated vault entries for secret fields.

Path Parameters

  • interface_name (required): Name of the integration

Response

{ "deleted": true }

Error Responses

  • 404 Not Found: Interface not found or no config exists

Get Integration Details

GET /api/interfaces/{interface_name}

Get detailed information about a specific integration. Returns an InterfaceDefinition.

Path Parameters

  • interface_name (required): Name of the integration

Response

{
"name": "webhook",
"description": "Generic webhook handler",
"version": "1.0.0",
"actions": [
{
"name": "receive",
"description": "Receive and process a webhook payload",
"parameters": [
{"name": "source", "type": "string", "description": "Webhook source identifier", "required": true},
{"name": "payload", "type": "object", "description": "Webhook payload data", "required": true},
{"name": "create_task", "type": "boolean", "description": "Create a task from this webhook", "required": false, "default": true}
],
"ai_access": "execute",
"side_effect": "write",
"returns": {},
"idempotent": false,
"timeout_seconds": 30
},
{
"name": "send",
"description": "Send a webhook to an external URL",
"parameters": [...],
"ai_access": "propose",
"side_effect": "write",
"returns": {},
"idempotent": false,
"timeout_seconds": 30
}
],
"events": []
}

Error Responses

  • 404 Not Found: Integration does not exist

Execute Integration Action

POST /api/interfaces/execute

Execute a specific action on an integration. Takes an ActionExecution request body (not path parameters).

Request Body

{
"interface_name": "echo",
"action_name": "echo",
"parameters": {
"message": "Hello World"
},
"user_id": "user-uuid",
"group_id": "group-uuid"
}

Required Fields

  • interface_name (string): Name of the integration
  • action_name (string): Name of the action to execute
  • user_id (string): User performing the action
  • group_id (string): Group context

Optional Fields

  • parameters (object): Action parameters (default: {})
  • task_id (string): Associated task ID

Response (ActionResult)

{
"success": true,
"result": {
"message": "Hello World",
"original": "Hello World"
},
"error": null,
"execution_time_ms": 2,
"metadata": {}
}

Error Responses

  • 404 Not Found: Integration or action does not exist
  • 422 Unprocessable Entity: Missing required fields
  • 500 Internal Server Error: Action execution failed

Examples

# Echo Integration
curl -X POST http://localhost:8000/api/interfaces/execute \
-H "Content-Type: application/json" \
-d '{
"interface_name": "echo",
"action_name": "echo",
"parameters": {"message": "Hello World"},
"user_id": "test-user",
"group_id": "test-group"
}'

# Webhook Integration
curl -X POST http://localhost:8000/api/interfaces/execute \
-H "Content-Type: application/json" \
-d '{
"interface_name": "webhook",
"action_name": "receive",
"parameters": {"source": "github", "payload": {"event": "push"}},
"user_id": "test-user",
"group_id": "test-group"
}'

WebSocket API

Authentication Required: WebSocket connections require JWT authentication.

Group Isolation: Events are automatically filtered by the authenticated user's group. Users only receive events relevant to their group.

Real-Time Event Stream (Query Param Auth)

WS /ws/tasks?token=<jwt_access_token>

Connect to the WebSocket endpoint to receive real-time task events. Authentication is required via JWT token in query parameter.

Real-Time Event Stream (Message Auth) — V1.3

WS /ws/tasks

Alternative WebSocket endpoint where authentication is sent in the first message body (not URL query string) to avoid token exposure in server/proxy logs and browser history (M-AUTHN-002).

First Message (Required):

{ "type": "auth", "token": "<JWT>" }

Auth Response:

{ "type": "auth", "status": "ok" }

Events streamed after auth: task.*, notification.*, component.* — all filtered by user's groups and user_id.

Authentication

The WebSocket connection requires:

  1. Valid JWT access token (from /api/auth/signin or /api/auth/signup)
  2. User must be a member of a group
  3. Token must not be expired

Connection Example (JavaScript)

const accessToken = 'eyJhbGciOiJIUzI1NiIs...';

const ws = new WebSocket(`ws://localhost:8000/ws/tasks?token=${accessToken}`);

ws.onopen = () => {
console.log('Connected to Morphee WebSocket');
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Event received:', data);
};

ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
// code 1008 = policy violation (auth failed)
};

Connection Rejection

If authentication fails, the connection will be closed with:

  • Code 1008 (Policy Violation)
  • Reason: "Missing authentication token", "Invalid authentication token", or "User must be in a group"

Event Format

{
"event_type": "task.created",
"channel": "task:created",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"description": "New task"
},
"timestamp": "2026-02-09T12:00:00Z"
}

Event Types

All events are automatically filtered by group — you will only receive events for tasks belonging to your group.

Task Events

  • task.created: New task created in your group
  • task.updated: Task metadata updated
  • task.status_changed: Task status changed
  • task.deleted: Task deleted

Action Events

  • action.executed: Integration action completed successfully
  • action.failed: Integration action failed

Group Filtering

Events include a group_id field in their data payload. The WebSocket server automatically filters events to only send those matching your authenticated user's group.


Data Models

Task

{
id: string; // UUID
description: string; // Task description
status: TaskStatus; // Task status enum
metadata: object; // Additional task data
created_at: string; // ISO 8601 datetime
updated_at: string; // ISO 8601 datetime
}

TaskStatus Enum

  • pending: Task created, awaiting processing
  • running: Task is currently being processed
  • completed: Task finished successfully
  • failed: Task encountered an error
  • cancelled: Task was cancelled
  • paused: Task execution paused

ActionDefinition

{
name: string; // Action identifier
description: string; // Action description
parameters: ActionParameter[]; // Input parameters
ai_access: "execute" | "propose" | "blocked"; // How AI can use this action
side_effect: "read" | "write" | "delete"; // What the action does
returns: object; // JSON Schema for output shape
idempotent: boolean; // Safe to retry on failure?
timeout_seconds: number; // Execution timeout
}

EventDefinition

{
name: string; // Event identifier (e.g. "email_received")
description: string; // Event description
payload_schema: object; // JSON Schema for event data
}

ActionResult

{
success: boolean; // Execution success status
result: object | null; // Action result data (null on failure)
error: string | null; // Error message if failed
execution_time_ms: number; // Execution time in milliseconds
metadata: object; // Additional execution metadata
}

Chat API

Authentication Required: All chat endpoints require authentication via JWT token.

Group Isolation: Conversations are automatically filtered by the authenticated user's group.

Send Message (SSE Streaming)

POST /api/chat/

Send a message and receive an AI response streamed via Server-Sent Events (SSE). Creates a new conversation if no conversation_id is provided.

Headers

Authorization: Bearer <access_token>

Request Body

{
"content": "Hello, Morphee!",
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
"attachment": {
"name": "photo.jpg",
"type": "image/jpeg",
"data": "data:image/jpeg;base64,/9j/4AAQ..."
}
}

Required Fields

  • content (string, 1-32000 chars): Message content

Optional Fields

  • conversation_id (UUID): Existing conversation to continue. If omitted, a new conversation is created.
  • attachment (object): Image attachment for Claude Vision analysis
    • name (string, max 255 chars): File name
    • type (string): MIME type — image/jpeg, image/png, image/gif, or image/webp
    • data (string): Base64-encoded image data (with or without data: URL prefix). Max 20 MB.
    • Images are automatically resized to max 1568px (longest side) before sending to the LLM.

Response (200 OK, text/event-stream)

The response is a stream of SSE events:

event: token
data: "Hello"

event: token
data: " there!"

event: title
data: "Hello, Morphee!"

event: done
data: {"conversation_id": "550e8400-...", "message_id": "660e8400-..."}

Event Types

EventDataDescription
tokenstringA text chunk of the AI response
tool_useJSON objectLLM is calling a tool: {id, tool, input}
tool_resultJSON objectTool execution result: {id, tool, success, result?, error?}
approval_requestJSON objectAction requires user approval: {approval_id, id, tool, input}
titlestringAuto-generated conversation title (new conversations only)
doneJSON objectCompletion signal with conversation_id and message_id
errorJSON objectError with message field

Tool Call Example

When the AI calls a tool (e.g., creating a task), the SSE stream includes:

event: token
data: "Let me create that task for you."

event: tool_use
data: {"id": "tool_1", "tool": "tasks__create_task", "input": {"description": "Buy groceries"}}

event: tool_result
data: {"id": "tool_1", "tool": "tasks__create_task", "success": true, "result": {"task_id": "..."}}

event: token
data: "Done! I've created the task."

event: done
data: {"conversation_id": "...", "message_id": "..."}

Status Codes

  • 200 OK: Stream started successfully
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group

List Conversations

GET /api/chat/conversations

Retrieve all conversations for the authenticated user, ordered by most recently updated.

Headers

Authorization: Bearer <access_token>

Query Parameters

  • limit (optional): Maximum number of conversations to return (default: 50, min: 1, max: 100)
  • offset (optional): Number of conversations to skip (default: 0)
  • archived (optional): Include archived conversations (default: false)
  • space_id (optional): Filter conversations by Space UUID

Response (200 OK)

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "660e8400-e29b-41d4-a716-446655440001",
"group_id": "770e8400-e29b-41d4-a716-446655440002",
"space_id": null,
"title": "Hello, Morphee!",
"message_count": 2,
"created_at": "2026-02-11T10:00:00Z",
"updated_at": "2026-02-11T10:01:00Z"
}
]

**Status Codes**

- `200 OK`: Conversations retrieved
- `401 Unauthorized`: Missing or invalid token
- `403 Forbidden`: User not in a group

---

### Get Conversation Messages

```http
GET /api/chat/conversations/{conversation_id}/messages

Retrieve messages in a conversation with optional pagination.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation

Query Parameters

  • limit (optional, int): Maximum messages to return (default: 50)
  • offset (optional, int): Number of messages to skip for pagination (default: 0)

Response (200 OK)

[
{
"id": "msg-1",
"conversation_id": "550e8400-...",
"role": "user",
"content": "Hello, Morphee!",
"token_count": null,
"created_at": "2026-02-11T10:00:00Z"
},
{
"id": "msg-2",
"conversation_id": "550e8400-...",
"role": "assistant",
"content": "Hello there! How can I help you today?",
"token_count": null,
"created_at": "2026-02-11T10:00:01Z"
}
]

Status Codes

  • 200 OK: Messages retrieved
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Conversation does not exist

Update Conversation

PATCH /api/chat/conversations/{conversation_id}

Update a conversation's metadata (title, pinned status, archived status).

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation

Request Body

{
"title": "My renamed conversation",
"is_pinned": true,
"is_archived": false
}

Updatable Fields

  • title (string): Conversation title
  • is_pinned (boolean): Pin conversation to top of list
  • is_archived (boolean): Archive conversation (hidden from default list)

Response (200 OK)

Returns the updated conversation object.

Status Codes

  • 200 OK: Conversation updated
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Conversation does not exist

Delete Conversation

DELETE /api/chat/conversations/{conversation_id}

Delete a conversation and all its messages permanently.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation

Response (200 OK)

{
"status": "deleted"
}

Status Codes

  • 200 OK: Conversation deleted
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Conversation does not exist

Decide on Approval Request

POST /api/chat/approve/{approval_id}

Approve or reject a pending tool action that requires user approval (actions with ai_access: "propose"). The approval_id is provided in the approval_request SSE event during a chat stream.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • approval_id (required): The approval ID from the approval_request SSE event

Request Body

{
"action": "approve"
}

Fields

  • action (required): "approve" or "reject"

Response (200 OK)

{
"status": "approved",
"approval_id": "abc-123"
}

Status Codes

  • 200 OK: Decision recorded, tool execution resumes
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Approval request not found or expired (120s timeout)

Approval Flow

  1. During a chat stream, the AI may call a tool with ai_access: "propose" (e.g., webhook__send)
  2. The SSE stream emits an approval_request event with {approval_id, id, tool, input}
  3. Frontend displays an approval UI to the user
  4. User clicks Approve or Reject → POST /api/chat/approve/{approval_id}
  5. If approved, the tool executes and results stream back. If rejected, the AI is told the action was rejected.

Handle Component Event

POST /api/chat/component-event

Handle an interactive component event (e.g., form submission, button click). For ai_turn events, the event is formatted as a synthetic user message and triggers a new AI response streamed via SSE.

Headers

Authorization: Bearer <access_token>

Request Body

{
"source": {
"component_id": "comp-uuid",
"component_type": "form",
"conversation_id": "conv-uuid"
},
"event_type": "submit",
"payload": {
"name": "John",
"email": "john@example.com"
},
"tier": "ai_turn"
}

Fields

  • source (required): Object with component_id (string), component_type (string), conversation_id (string)
  • event_type (required, string): The event name (e.g., "submit", "click", "select", "confirm", "cancel", "dismiss")
  • payload (optional, object): Event-specific data (default: {})
  • tier (optional, string): "local" or "ai_turn" (default: "ai_turn")

Response (200 OK, text/event-stream)

For ai_turn tier, returns an SSE stream identical to the chat endpoint:

event: token
data: "I received your form submission."

event: tool_use
data: {"id": "tool_1", "tool": "tasks__create_task", "input": {...}}

event: tool_result
data: {"id": "tool_1", "tool": "tasks__create_task", "success": true, "result": {...}}

event: token
data: "Done! I've created the task."

event: done
data: {"conversation_id": "...", "message_id": "..."}

Status Codes

  • 200 OK: Stream started successfully
  • 400 Bad Request: Invalid tier or missing fields
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group

Event Tiers

TierTransportDescription
localNone (frontend only)Handled entirely in frontend (dismiss, collapse). Should not reach this endpoint.
ai_turnSSE streamInjected as user message, triggers new AI turn with full context

Edit Message

PATCH /api/chat/conversations/{conversation_id}/messages/{message_id}

Edit a user message in a conversation. Only user-role messages can be edited.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation
  • message_id (required): UUID of the message to edit

Request Body

{
"content": "Updated message content"
}

Required Fields

  • content (string, 1-32000 chars): The new message content

Response (200 OK)

Returns the updated message object with edited_at timestamp set.

Status Codes

  • 200 OK: Message updated
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group or message is not a user message
  • 404 Not Found: Conversation or message does not exist

Regenerate Response (SSE Streaming)

POST /api/chat/conversations/{conversation_id}/regenerate

Regenerate the last assistant response in a conversation. Deletes the last assistant message and streams a new AI response via SSE.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation

Response (200 OK, text/event-stream)

Returns an SSE stream identical to the Send Message endpoint:

event: token
data: "Here is"

event: token
data: " a new response."

event: done
data: {"conversation_id": "...", "message_id": "..."}

Status Codes

  • 200 OK: Regeneration stream started
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Conversation does not exist or has no assistant messages

Pin Message

POST /api/chat/conversations/{conversation_id}/messages/{message_id}/pin

Pin a message in a conversation. Maximum 5 pinned messages per conversation.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation
  • message_id (required): UUID of the message to pin

Response (200 OK)

Returns the updated Message object with pinned: true.

Status Codes

  • 200 OK: Message pinned
  • 400 Bad Request: Pin limit reached (max 5) or message not found
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group

Unpin Message

DELETE /api/chat/conversations/{conversation_id}/messages/{message_id}/pin

Unpin a previously pinned message.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation
  • message_id (required): UUID of the message to unpin

Response (200 OK)

Returns the updated Message object with pinned: false.

Status Codes

  • 200 OK: Message unpinned
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group
  • 404 Not Found: Message does not exist

Get Pinned Messages

GET /api/chat/conversations/{conversation_id}/messages/pinned

Get all pinned messages for a conversation.

Headers

Authorization: Bearer <access_token>

Path Parameters

  • conversation_id (required): UUID of the conversation

Response (200 OK)

[
{
"id": "msg-uuid",
"conversation_id": "conv-uuid",
"role": "user",
"content": "Important message",
"created_at": "2026-02-14T10:00:00Z",
"pinned": true
}
]

Status Codes

  • 200 OK: Pinned messages returned (may be empty array)
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group

Add Message Reaction

POST /api/chat/conversations/{conversation_id}/messages/{message_id}/react

Add an emoji reaction to a message. The emoji is stored in message metadata.

Request Body: { "emoji": "👍" }

Response (200 OK): Updated message object.


Remove Message Reaction

DELETE /api/chat/conversations/{conversation_id}/messages/{message_id}/react

Remove an emoji reaction from a message.

Request Body: { "emoji": "👍" }

Response (200 OK): Updated message object.


Sync Local Conversation

POST /api/chat/conversations/sync-local

Persist a local LLM conversation turn (user + assistant messages) to the backend. Used by V1.5 local LLM: conversations happen client-side via Tauri candle, this endpoint syncs them to PostgreSQL + Git.

Request Body:

{
"conversation_id": "uuid | null",
"space_id": "uuid | null",
"user_content": "string (max 100,000 chars)",
"assistant_content": "string (max 100,000 chars)"
}

If conversation_id is null, creates a new conversation.

Response (200 OK):

{
"conversation_id": "uuid",
"user_message_id": "uuid",
"assistant_message_id": "uuid"
}

Onboarding API

Authentication Required: Requires JWT authentication, but does NOT require group membership (uses get_current_user, not require_group_access).

The onboarding conversation is ephemeral — the frontend sends the full message history with each request. Nothing is persisted to the database.

Send Onboarding Message (SSE Streaming)

POST /api/onboarding/chat

Send a message during onboarding and receive a streaming AI response via SSE. The AI guides the user through persona detection, group creation, and default space setup.

Headers

Authorization: Bearer <access_token>

Request Body

{
"content": "I'm a parent with two kids",
"history": [
{"role": "assistant", "content": "Welcome! Tell me about yourself."},
{"role": "user", "content": "Hi there!"},
{"role": "assistant", "content": "What kind of group are you setting up?"}
]
}

Required Fields

  • content (string): The user's message

Optional Fields

  • history (array): Previous messages in the conversation. Each item has role ("user" or "assistant") and content (string). Defaults to empty array.

Response (200 OK, text/event-stream)

Same SSE event types as the Chat API:

EventDataDescription
tokenstringA text chunk of the AI response
tool_useJSON objectLLM is calling a tool: {id, tool, input}
tool_resultJSON objectTool execution result: {id, tool, success, result?, error?}
doneJSON objectCompletion signal with stop_reason
errorJSON objectError with message field

Available Tools

The onboarding AI has access to three tools:

  • onboarding__create_group — Creates the user's group (name, timezone)
  • onboarding__create_spaces — Creates default spaces for the group
  • onboarding__complete — Signals onboarding is complete (returns _onboarding_complete: true)

Status Codes

  • 200 OK: Stream started successfully
  • 400 Bad Request: User already has a group
  • 401 Unauthorized: Missing or invalid token
  • 422 Unprocessable Entity: Missing required fields

Onboarding Flow

  1. User signs up → group_id is null → frontend redirects to /onboarding
  2. Frontend sends messages to POST /api/onboarding/chat with full history
  3. AI detects persona, asks for group name, creates group + spaces via tools
  4. onboarding__complete tool returns {_onboarding_complete: true}
  5. Frontend detects completion, calls GET /api/auth/me to refresh user data
  6. User now has group_id → frontend navigates to /

Notification API

Authentication Required: All notification endpoints require authentication via JWT token.

List Notifications

GET /api/notifications/

Retrieve notifications for the authenticated user.

Query Parameters

  • unread_only (optional, bool): Filter to unread only (default: false)
  • limit (optional, int): Maximum notifications to return (default: 50, max: 100)

Response (200 OK)

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"user_id": "770e8400-e29b-41d4-a716-446655440002",
"title": "Reminder: Buy groceries",
"body": "You asked me to remind you at 5pm",
"type": "reminder",
"source": "cron",
"source_id": "880e8400-e29b-41d4-a716-446655440003",
"read": false,
"read_at": null,
"created_at": "2026-02-11T17:00:00Z"
}
]

Get Unread Count

GET /api/notifications/unread-count

Get the count of unread notifications for the authenticated user.

Response (200 OK)

{
"unread_count": 3
}

Mark Notification as Read

POST /api/notifications/{notification_id}/read

Mark a single notification as read.

Response (200 OK): Returns the updated Notification object.

Status Codes

  • 200 OK: Notification marked as read
  • 404 Not Found: Notification not found

Mark All Notifications as Read

POST /api/notifications/read-all

Mark all notifications for the authenticated user as read.

Response (200 OK)

{
"marked_read": 5
}

Push Notifications API

Authentication Required: All push notification endpoints require authentication via JWT token.

Register Push Token

POST /api/push/register

Register a device push token for receiving push notifications. Supports APNs (iOS), FCM (Android), and web push tokens.

Request Body

{
"platform": "ios",
"token": "device-push-token-string"
}

Fields

  • platform (required): "ios", "android", or "web"
  • token (required): The device push token (APNs device token, FCM registration token, or web push subscription)

Response (200 OK)

{
"registered": true,
"token_id": "uuid-of-registered-token"
}

Status Codes

  • 200 OK: Token registered (or already exists)
  • 401 Unauthorized: Missing or invalid auth token
  • 422 Unprocessable Entity: Invalid platform or missing token

Unregister Push Token

DELETE /api/push/unregister

Remove a device push token (e.g., on logout or app uninstall).

Request Body

{
"token": "device-push-token-string"
}

Response (200 OK)

{
"unregistered": true
}

If the token was not found, returns {"unregistered": false}.

Status Codes

  • 200 OK: Token removed (or not found)
  • 401 Unauthorized: Missing or invalid auth token

OAuth API

Start Google OAuth Flow

GET /api/oauth/google/authorize

Start the Google OAuth2 authorization flow. Returns the Google authorization URL for the frontend to redirect to (full-page redirect, not popup).

Query Parameters

  • scopes (optional, string): Comma-separated scope names (default: "calendar,gmail")
  • redirect_to (optional, string): URL to redirect back to after callback. Default: {FRONTEND_URL}/settings. For Tauri desktop: http://tauri.localhost/settings. For mobile: morphee://settings. Allowed schemes: http, https, morphee.

Response (200 OK)

{
"authorize_url": "https://accounts.google.com/o/oauth2/v2/auth?..."
}

Status Codes

  • 200 OK: Authorization URL generated
  • 400 Bad Request: Invalid redirect URI scheme
  • 501 Not Implemented: Google OAuth not configured (missing GOOGLE_CLIENT_ID)

Google OAuth Callback

GET /api/oauth/google/callback

Handle Google OAuth2 callback. Validates state, exchanges code for tokens, stores tokens in vault. Redirects back to the frontend settings page with success/error query parameters.

Query Parameters

  • code (string): Authorization code from Google
  • state (string): HMAC-signed state parameter (contains user_id, group_id, optional redirect_to)
  • error (string): Error from Google (if any)

Response: HTTP 302 redirect to {redirect_to}?oauth=success&provider=google&email={email} on success, or {redirect_to}?oauth=error&provider=google&error={msg} on failure.


Check Google Connection Status

GET /api/oauth/google/status

Check if the current user has a connected Google account.

Response (200 OK)

{
"connected": true,
"account_email": "user@gmail.com",
"scopes": ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/gmail.modify"],
"connected_at": "2026-02-11T10:00:00Z"
}

Disconnect Google Account

POST /api/oauth/google/disconnect

Disconnect Google account — removes tokens from vault and DB record.

Response (200 OK)

{
"disconnected": true
}

Skills API

Authentication Required: All skills endpoints require authentication via JWT token.

Group Isolation: Skills are automatically filtered by the authenticated user's group.

List Skills

GET /api/skills/

Retrieve all skills for the authenticated user's group.

Query Parameters

  • enabled_only (optional, bool): Filter to enabled skills only (default: false)
  • limit (optional, int): Maximum skills to return (default: 50, max: 100)

Response (200 OK)

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"group_id": "660e8400-e29b-41d4-a716-446655440001",
"space_id": null,
"created_by": "770e8400-e29b-41d4-a716-446655440002",
"name": "weekly_summary",
"description": "Fetch tasks and send a summary notification",
"definition": {
"parameters": [],
"steps": [
{"id": "fetch", "integration": "tasks", "action": "list", "params": {"status": "completed"}},
{"id": "notify", "integration": "notifications", "action": "send", "params": {"title": "Weekly Summary", "body": "{{steps.fetch.result.count}} tasks completed"}}
]
},
"enabled": true,
"run_count": 5,
"last_run_at": "2026-02-11T09:00:00Z",
"created_at": "2026-02-10T10:00:00Z",
"updated_at": "2026-02-10T10:00:00Z"
}
]

Get Skill

GET /api/skills/{skill_id}

Get details of a specific skill.

Response (200 OK): Returns a single skill object (same shape as list items).

Status Codes

  • 200 OK: Skill retrieved
  • 404 Not Found: Skill not found

Create Skill

POST /api/skills/

Create a new dynamic skill. The definition is validated against registered integrations.

Request Body

{
"name": "weekly_summary",
"description": "Fetch tasks and send a summary notification",
"space_id": null,
"definition": {
"parameters": [
{"name": "status", "type": "string", "description": "Task status to filter", "required": false, "default": "completed"}
],
"steps": [
{"id": "fetch", "integration": "tasks", "action": "list", "params": {"status": "{{params.status}}"}},
{"id": "notify", "integration": "notifications", "action": "send", "params": {"title": "Summary", "body": "{{steps.fetch.result.count}} tasks"}}
]
}
}

Template Variables

  • {{params.name}} — skill input parameter
  • {{steps.step_id.result.field}} — output from a previous step
  • {{context.user_id}}, {{context.group_id}} — execution context

Response (201 Created): Returns the created skill object.

Status Codes

  • 201 Created: Skill created and registered as a dynamic interface
  • 400 Bad Request: Invalid definition (referenced integration/action doesn't exist)
  • 409 Conflict: Skill with this name already exists in the group

Update Skill

PUT /api/skills/{skill_id}

Update an existing skill. If the definition changes, it is re-validated.

Request Body (all fields optional)

{
"name": "updated_name",
"description": "Updated description",
"enabled": false,
"definition": { ... }
}

Response (200 OK): Returns the updated skill object.

Status Codes

  • 200 OK: Skill updated
  • 400 Bad Request: Invalid definition
  • 404 Not Found: Skill not found

Delete Skill

DELETE /api/skills/{skill_id}

Delete a skill and unregister its dynamic interface.

Response (200 OK)

{
"deleted": true,
"skill_id": "550e8400-e29b-41d4-a716-446655440000"
}

Status Codes

  • 200 OK: Skill deleted
  • 404 Not Found: Skill not found

Search API

Authentication Required: All search endpoints require authentication via JWT token.

Group Isolation: Search results are automatically filtered by the authenticated user's group.

GET /api/search/

Search across conversations, tasks, and spaces using case-insensitive text matching (ILIKE). Useful for the Cmd+K search dialog.

Headers

Authorization: Bearer <access_token>

Query Parameters

  • q (required, string): Search query (minimum 1 character, max 200)
  • types (optional, string): Comma-separated list of result types to include. Options: conversations, tasks, spaces. Default: "conversations,tasks,spaces".
  • limit (optional, int): Maximum total results (default: 20, max: 50). Distributed across types.

Response (200 OK)

{
"results": [
{
"id": "550e8400-...",
"type": "conversation",
"title": "Planning weekend trip",
"snippet": null
},
{
"id": "660e8400-...",
"type": "task",
"title": "Buy groceries for the trip",
"snippet": "pending"
},
{
"id": "770e8400-...",
"type": "space",
"title": "Family Trips",
"snippet": "Planning and organizing family trips"
}
],
"total": 3
}

Result Fields

  • id (string): Entity UUID
  • type (string): "conversation", "task", or "space"
  • title (string): Display title (conversation title, task description, or space name)
  • snippet (string | null): Additional context (task status, space description)

Status Codes

  • 200 OK: Search results returned (results array may be empty)
  • 400 Bad Request: Missing or empty query parameter
  • 401 Unauthorized: Missing or invalid token
  • 403 Forbidden: User not in a group

ACL & Permissions API

Authentication Required: All endpoints require JWT authentication and group membership.

The ACL system provides generic resource-based access control, user preferences, and bilateral monitoring permissions.

Grant Permission

POST /api/acl/grant

Grant or deny permission on a resource. Only the resource owner can grant permissions.

Headers

Authorization: Bearer <access_token>

Request Body

{
"resource_type": "space",
"resource_id": "resource-uuid",
"principal_id": "user-uuid",
"operation": "read",
"permission": "allow"
}

Required Fields

  • resource_type (string): One of space, conversation, task, integration, action, memory
  • resource_id (string): UUID of the resource
  • principal_id (UUID): User receiving the permission
  • operation (string): One of read, write, delete, execute, *
  • permission (string): allow or deny

Response (200 OK)

Returns the created ResourceACL object.

Status Codes

  • 200 OK: Permission granted
  • 403 Forbidden: Not the resource owner or not in a group

Revoke Permission

DELETE /api/acl/revoke

Revoke a permission. Only the resource owner can revoke.

Query Parameters

  • resource_type (required): Resource type
  • resource_id (required): Resource UUID
  • principal_id (required): User UUID
  • operation (required): Operation to revoke

Response (200 OK)

{ "success": true }

Status Codes

  • 200 OK: Permission revoked
  • 403 Forbidden: Not the resource owner or not in a group

Check Permission

GET /api/acl/check

Check if the current user has permission for an operation on a resource.

Query Parameters

  • resource_type (required): Resource type
  • resource_id (required): Resource UUID
  • operation (required): Operation to check

Response (200 OK)

{
"allowed": true,
"reason": "Owner of resource",
"source": "owner"
}

Status Codes

  • 200 OK: Check result returned
  • 403 Forbidden: Not in a group

List ACLs

GET /api/acl/list

List all ACL entries for a resource.

Query Parameters

  • resource_type (required): Resource type
  • resource_id (required): Resource UUID

Response (200 OK)

Returns array of ResourceACL objects.

Status Codes

  • 200 OK: ACLs returned
  • 403 Forbidden: Not in a group

Get User Preferences

GET /api/acl/preferences

Get the current user's preferences (AI behavior, content filtering, screen time).

Response (200 OK)

{
"id": "pref-uuid",
"user_id": "user-uuid",
"content_filter_level": "moderate",
"ai_personality_mode": "standard",
"screen_time_limit_mins": null,
"quiet_hours_start": null,
"quiet_hours_end": null
}

Returns null if no preferences are set.


Update User Preferences

PUT /api/acl/preferences

Update the current user's preferences. Creates preferences if they don't exist.

Request Body (all fields optional)

{
"content_filter_level": "strict",
"ai_personality_mode": "child_friendly",
"screen_time_limit_mins": 120,
"quiet_hours_start": "21:00",
"quiet_hours_end": "07:00"
}

Optional Fields

  • content_filter_level (string): strict, moderate, or none
  • ai_personality_mode (string): child_friendly, professional, educational, or standard
  • screen_time_limit_mins (integer): Daily screen time limit in minutes
  • quiet_hours_start (time): Start of quiet hours (HH:MM)
  • quiet_hours_end (time): End of quiet hours (HH:MM)

Response (200 OK)

Returns the updated UserPreferences object.


List Monitoring Requests (Sent)

GET /api/acl/monitoring/requested

List monitoring permissions where the current user is the requester (parent/supervisor).

Response (200 OK)

Returns array of MonitoringPermission objects.


List Monitoring Requests (Received)

GET /api/acl/monitoring/received

List monitoring permissions where the current user is the target (child/supervisee).

Response (200 OK)

Returns array of MonitoringPermission objects.


Request Monitoring

PUT /api/acl/monitoring/request/{target_id}

Request monitoring permission for another user. Creates or updates the monitoring permission.

Path Parameters

  • target_id (required): UUID of the user to monitor

Request Body

{
"requests_summaries": true,
"requests_full_access": false
}

Response (200 OK)

Returns the MonitoringPermission object. Effective access requires bilateral consent (both requests_* AND allows_* must be true).

Status Codes

  • 200 OK: Request created/updated
  • 400 Bad Request: Cannot monitor yourself
  • 403 Forbidden: Not in a group

PUT /api/acl/monitoring/consent/{requester_id}

Grant or deny consent to a monitoring request (target side).

Path Parameters

  • requester_id (required): UUID of the user requesting monitoring

Request Body

{
"allows_summaries": true,
"allows_full_access": false
}

Response (200 OK)

Returns the MonitoringPermission object with computed effective_summary_access and effective_full_access fields.

Status Codes

  • 200 OK: Consent updated
  • 400 Bad Request: Cannot monitor yourself
  • 403 Forbidden: Not in a group

Git .acl File Format

The ACL system supports Git-based permissions via .acl files stored in memory repositories. These files use YAML format and support directory inheritance — a .acl file applies to its directory and all subdirectories unless overridden.

Location: {memory_git_path}/{group_id}/<any-directory>/.acl

Format:

acls:
- principal: "user-uuid" # User UUID or email address
operation: "read" # read, write, delete, execute, or * (wildcard)
permission: "allow" # allow or deny
- principal: "jane@example.com"
operation: "*"
permission: "deny"

Fields per entry:

FieldTypeValuesDescription
principalstringUUID or emailUser identified by UUID or email address
operationstringread, write, delete, execute, *Operation to control. * matches all operations
permissionstringallow, denyWhether to grant or deny access

Resolution order (first match wins):

  1. Walk from the resource's directory up to the repository root
  2. At each level, check for a .acl file
  3. Within each file, match by principal (UUID or email) and operation (exact or wildcard)
  4. If no .acl file matches, fall back to database ACLs

Example — restrict a child's diary to read-only for parents:

memories/
├── family/
│ └── .acl # Everyone in group can read/write
└── emma-diary/
└── .acl # Only Emma can write; parents can read

memories/emma-diary/.acl:

acls:
- principal: "emma-uuid"
operation: "*"
permission: "allow"
- principal: "parent-uuid"
operation: "read"
permission: "allow"
- principal: "parent-uuid"
operation: "write"
permission: "deny"

Memory API (Extended — V1.0)

Authentication Required: All memory endpoints require authentication via JWT token.

Group Isolation: Memory operations are scoped to the authenticated user's group.

Create Memory Branch

POST /api/memory/branches

Create a new git branch in the group's memory repository.

Request Body

{
"name": "vacation-planning",
"from_commit": "a1b2c3d4"
}

Fields

  • name (required, string): Branch name (alphanumeric + hyphens, max 50 chars)
  • from_commit (optional, string): Commit hash to branch from (default: HEAD)

Response (201 Created)

{
"branch": "vacation-planning",
"commit": "a1b2c3d4",
"created_at": "2026-02-13T12:00:00Z"
}

Switch Memory Branch

POST /api/memory/branches/switch

Switch the active memory branch for the group. All subsequent memory operations (RAG, store, recall) use this branch.

Request Body

{
"branch": "vacation-planning"
}

Response (200 OK)

{
"branch": "vacation-planning",
"commit": "a1b2c3d4",
"switched_at": "2026-02-13T12:01:00Z"
}

List Memory Branches

GET /api/memory/branches

List all branches in the group's memory repository.

Response (200 OK)

{
"branches": [
{"name": "main", "commit": "e5f6g7h8", "active": true},
{"name": "vacation-planning", "commit": "a1b2c3d4", "active": false},
{"name": "archived-2025", "commit": "i9j0k1l2", "active": false}
]
}

Search Commit History

POST /api/memory/search-history

Search git commits by semantic query, time range, and branch.

Request Body

{
"query": "vacation planning",
"time_range": {
"start": "2026-01-01T00:00:00Z",
"end": "2026-02-13T23:59:59Z"
},
"branch": "vacation-planning",
"limit": 10
}

Fields

  • query (required, string): Semantic search query
  • time_range (optional, object): Filter by commit timestamp
  • branch (optional, string): Search within specific branch (default: active branch)
  • limit (optional, int): Max commits to return (default: 10, max: 50)

Response (200 OK)

{
"results": [
{
"commit": "a1b2c3d4",
"message": "Add vacation destination ideas",
"timestamp": "2026-02-10T15:30:00Z",
"branch": "vacation-planning",
"similarity": 0.92,
"changed_files": ["conversations/conv-123.md", "facts/fact-456.md"]
}
],
"total": 3
}

Merge Memory Branch

POST /api/memory/branches/merge

Merge one branch into another. Requires approval if conflicts detected.

Request Body

{
"source": "vacation-planning",
"target": "main",
"strategy": "ours"
}

Fields

  • source (required, string): Branch to merge from
  • target (required, string): Branch to merge into
  • strategy (optional, string): Conflict resolution — "ours" (keep target), "theirs" (prefer source), "manual" (return conflicts for user resolution). Default: "manual".

Response (200 OK — no conflicts)

{
"merged": true,
"commit": "m1n2o3p4",
"merged_count": 5,
"conflicts": []
}

Response (200 OK — conflicts detected, manual strategy)

{
"merged": false,
"conflicts": [
{
"file": "facts/vacation-dest.md",
"ours": "Paris",
"theirs": "Rome"
}
]
}

Settings API (V0.9)

Authentication Required: All settings endpoints require authentication via JWT token.

Get Setting

GET /api/settings/{category}/{key}

Get a specific setting value.

Path Parameters

  • category (required): Setting category (profile, group, notifications, privacy, appearance, integrations)
  • key (required): Setting key (e.g., name, timezone, theme)

Response (200 OK)

{
"category": "profile",
"key": "name",
"value": "Jane Doe",
"value_type": "string",
"updated_at": "2026-02-13T10:00:00Z"
}

Update Setting

PUT /api/settings/{category}/{key}

Update a setting value. Some settings require parent role (group, integrations).

Request Body

{
"value": "John Doe"
}

Response (200 OK)

{
"category": "profile",
"key": "name",
"value": "John Doe",
"updated_at": "2026-02-13T12:00:00Z"
}

Status Codes

  • 200 OK: Setting updated
  • 403 Forbidden: Not authorized (e.g., child role trying to update group settings)

List Setting Categories

GET /api/settings/categories

List all available setting categories.

Response (200 OK)

{
"categories": [
{
"name": "profile",
"description": "User profile settings",
"keys": ["name", "email", "avatar_url", "language"]
},
{
"name": "group",
"description": "Group-wide settings (parent only)",
"keys": ["name", "timezone", "default_space_id"]
},
{
"name": "notifications",
"description": "Notification preferences",
"keys": ["types", "enabled", "quiet_hours", "sound"]
}
]
}

Extensions API (V1.2)

WASM extension system — catalog, install, manage, execute, and audit extensions.

Search Extension Catalog

GET /api/extensions/catalog?q=jira&category=productivity&limit=50&offset=0

Search the OCI extension registry.

Query Parameters

  • q (optional): Search query
  • category (optional): Filter by category
  • limit (optional, default 50, max 100): Results per page
  • offset (optional, default 0): Pagination offset

Response (200 OK)

{
"extensions": [
{
"id": "jira",
"name": "JIRA Integration",
"version": "1.0.0",
"description": "Manage JIRA issues from chat",
"author": "Morphee",
"downloads": 150
}
],
"total": 1
}

List Installed Extensions

GET /api/extensions/installed

List extensions installed for the user's group.

Response (200 OK)

[
{
"extension_id": "jira",
"name": "JIRA Integration",
"version": "1.0.0",
"description": "Manage JIRA issues from chat",
"author": "Morphee",
"enabled": true,
"permissions": ["http:read", "http:write", "vault:read"]
}
]

Install Extension

POST /api/extensions/install

Install an extension from the OCI registry. Pulls WASM binary, verifies signature, and stores.

Request Body

{
"extension_id": "jira",
"version": "1.0.0",
"approved_permissions": ["http:read", "http:write", "vault:read"]
}

Response (200 OK)

{
"extension_id": "jira",
"name": "JIRA Integration",
"version": "1.0.0",
"enabled": true
}

Status Codes

  • 200 OK: Extension installed
  • 400 Bad Request: Invalid manifest or permissions
  • 503 Service Unavailable: Extension system not initialized

Uninstall Extension

DELETE /api/extensions/{extension_id}

Remove an installed extension.

Response (200 OK)

{ "uninstalled": true }

Enable Extension

POST /api/extensions/{extension_id}/enable

Re-enable a disabled extension.

Response (200 OK)

{ "enabled": true }

Disable Extension

POST /api/extensions/{extension_id}/disable

Disable an extension without uninstalling it.

Response (200 OK)

{ "enabled": false }

Execute Extension Action

POST /api/extensions/{extension_id}/execute

Execute an action on an installed WASM extension. The extension runs in a sandboxed runtime with fuel limits, memory caps, and permission enforcement.

Request Body

{
"action": "create_issue",
"params": {
"project": "MORPH",
"summary": "Fix login bug",
"type": "Bug"
}
}

Response (200 OK)

{
"success": true,
"output": { "issue_key": "MORPH-123" },
"error": null,
"execution_time_ms": 45.2,
"fuel_consumed": 15000
}

Status Codes

  • 200 OK: Action executed (check success field)
  • 400 Bad Request: No group found
  • 503 Service Unavailable: Extension system not initialized

Get Extension Audit Log

GET /api/extensions/{extension_id}/audit?limit=50&offset=0

Get execution audit log and statistics for an extension.

Response (200 OK)

{
"entries": [
{
"extension_id": "jira",
"action": "create_issue",
"user_id": "uuid",
"group_id": "uuid",
"success": true,
"execution_time_ms": 45.2,
"fuel_consumed": 15000,
"timestamp": "2026-02-21T10:00:00Z"
}
],
"stats": {
"total_executions": 42,
"success_rate": 0.95,
"avg_execution_time_ms": 38.7
}
}

Check for Updates

GET /api/extensions/updates/check

Check all installed extensions for available updates from the registry.

Response (200 OK)

{
"updates": [
{
"extension_id": "jira",
"current_version": "1.0.0",
"latest_version": "1.1.0",
"changelog": "Bug fixes and performance improvements"
}
]
}

Update Extension

POST /api/extensions/{extension_id}/update

Update an extension to the latest version. Preserves existing permissions.

Response (200 OK)

{
"updated": true,
"extension_id": "jira"
}

Extension OAuth — Authorize

POST /api/extensions/{extension_id}/oauth/authorize

Start OAuth flow for an extension that requires external service authorization (e.g., JIRA, GitHub).

Response (200 OK)

{
"authorize_url": "https://auth.atlassian.com/authorize?client_id=..."
}

Extension OAuth — Callback

GET /api/extensions/{extension_id}/oauth/callback?code={code}&state={state}

OAuth callback handler. Exchanges authorization code for tokens and stores them in the vault.

Query Parameters

  • code (required): Authorization code from OAuth provider
  • state (required): CSRF state token for validation

Response (200 OK)

{
"connected": true,
"extension_id": "jira"
}

Extension OAuth — Status

GET /api/extensions/{extension_id}/oauth/status

Check whether an extension's OAuth connection is active.

Response (200 OK)

{
"connected": true,
"reason": null
}

Response (200 OK — Not Connected)

{
"connected": false,
"reason": "Token expired. Re-authorize to reconnect."
}

Identity API (V1.3)

Multi-modal identity — child identities, authentication methods, Morphee sessions.

Create Child Identity

POST /api/identity/children

Request Body:

{
"name": "Sophie",
"display_name": "Sophie",
"birthdate": "2018-03-15",
"pin": "1234",
"avatar_url": "https://example.com/avatar.jpg"
}

Only name is required. pin (4-6 digits) and avatar_url (HTTPS) are optional.

Response (201 Created):

{
"id": "uuid",
"name": "Sophie",
"display_name": "Sophie",
"role": "child",
"has_pin": true
}

List Children

GET /api/identity/children

Returns children in the caller's current group.

Delete Child

DELETE /api/identity/children/{child_id}

Only the creating parent or group admin can delete.

Authenticate as Child

POST /api/identity/auth/child

Request Body:

{
"child_user_id": "uuid",
"pin": "1234"
}

pin is required only if the child has a PIN set. Returns a Morphee JWT (iss=morphee).

Response (200 OK):

{
"access_token": "eyJ...",
"refresh_token": "...",
"user_id": "uuid",
"display_name": "Sophie",
"authentication_level": 1,
"expires_at": 1740000000,
"is_child": true
}

Refresh Child Session

POST /api/identity/auth/child/refresh

Request Body: { "refresh_token": "..." }

Register Biometric Method

POST /api/identity/{user_id}/biometric/register

Request Body: { "method_type": "face" } or { "method_type": "voice" }

Records a biometric auth method after Tauri enrollment succeeds. Templates are stored locally in the Tauri vault (never sent to backend).

Authenticate Child via Biometric

POST /api/identity/auth/child/biometric

Request Body: { "user_id": "uuid", "method_type": "face" } or { "user_id": "uuid", "method_type": "voice" }

Authenticates a child after Tauri has verified the biometric locally. Returns Morphee JWT.

Delete Biometric Method

DELETE /api/identity/{user_id}/biometric/{method_type}

Deletes the biometric auth method record. Client must also delete template from Tauri vault.

List Authentication Methods

GET /api/identity/methods

Add Authentication Method

POST /api/identity/methods

Request Body: { "method_type": "pin", "credential": "1234" }

Remove Authentication Method

DELETE /api/identity/methods/{method_id}

Cannot remove the last method.

List Active Sessions

GET /api/identity/sessions

Revoke Session

DELETE /api/identity/sessions/{session_id}

Revoke All Sessions

DELETE /api/identity/sessions

Switch to Parent Session

POST /api/identity/auth/switch-to-parent

Switch back to parent session using the adult httpOnly refresh cookie. Eliminates the need to store parent access token in sessionStorage during child sessions.

Auth: Uses httpOnly cookie (no Authorization header needed).

Response (200 OK):

{
"access_token": "eyJ...",
"refresh_token": "...",
"expires_in": 3600
}

MorpheeSelf API (V2.5+)

Status: PLANNED — Not Yet Implemented. These endpoints are designed but no backend routes exist yet.

Self-aware AI development endpoints. Morphee reads, understands, and improves her own codebase.

Search Code

POST /api/morphee-self/search-code

Search through Morphee's source code with context.

Request Body

{
"query": "async def validate_token",
"file_types": ["py", "ts", "rs"],
"max_results": 10
}

Response

{
"results": [
{
"file": "backend/auth/client.py",
"line": 42,
"match": "async def validate_token(token: str) -> dict:",
"context_before": ["", "class SupabaseAuthClient:"],
"context_after": [" # Validate JWT token", " try:"],
"url": "file:///morphee-beta/backend/auth/client.py#L42"
}
],
"total_found": 8
}

Status Codes

  • 200 OK: Search completed
  • 403 Forbidden: Self-awareness not enabled
  • 400 Bad Request: Invalid query or file types

Explain Implementation

POST /api/morphee-self/explain-implementation

Generate structured explanation of how a feature works.

Request Body

{
"feature": "authentication",
"detail_level": "medium"
}

Response

{
"feature": "authentication",
"files": [
"backend/auth/client.py",
"backend/api/auth.py",
"docs/architecture.md"
],
"code_snippets": {
"backend/auth/client.py": {
"lines": [42, 43, 44, 45, 46],
"content": "async def validate_token(token: str) -> dict:\n # Validate JWT token\n try:\n decoded = jwt.decode(token, self.jwt_secret)\n return decoded"
}
},
"detail_level": "medium"
}

Status Codes

  • 200 OK: Explanation generated
  • 403 Forbidden: Self-awareness not enabled
  • 404 Not Found: Feature not found

Get Architecture Diagram

GET /api/morphee-self/architecture?component={component}

Get system architecture information with code references.

Query Parameters

  • component (optional): "backend", "frontend", "rust", "full"

Response

{
"component": "backend",
"sections": [
{
"title": "Authentication",
"content": "Morphee uses Supabase Auth (GoTrue) for JWT-based authentication...",
"code_references": [
"backend/auth/client.py:15-89",
"backend/api/auth.py:1-120"
]
}
],
"diagram_url": "docs/architecture.md",
"last_updated": "2026-02-13T10:30:00Z"
}

Status Codes

  • 200 OK: Architecture retrieved
  • 403 Forbidden: Self-awareness not enabled

Review Community Branch

POST /api/morphee-self/review-branch

Morphee reviews a community-contributed branch. Requires human approval (propose action).

Request Body

{
"branch_name": "community/slack-integration",
"compare_to": "main"
}

Response

{
"branch": "community/slack-integration",
"author": "sarah-dev",
"commit_count": 12,
"files_changed": 8,
"analysis": {
"summary": "Adds Slack integration following BaseInterface pattern",
"architecture_impact": "Low - follows existing OAuth2 pattern",
"breaking_changes": [],
"security_concerns": []
},
"tests": {
"coverage_delta": "+3.2%",
"new_tests": 15,
"passing": true
},
"documentation": {
"updated": ["docs/interfaces.md", "docs/api.md"],
"missing": []
},
"recommendation": "approve",
"review_url": "https://github.com/morphee-app/morphee-beta/compare/main...community/slack-integration"
}

Status Codes

  • 200 OK: Review completed (requires user approval for execution)
  • 403 Forbidden: Self-improvement not enabled or user not authorized
  • 404 Not Found: Branch not found

Suggest Improvement

POST /api/morphee-self/suggest-improvement

Morphee proposes code improvement. Requires human approval (propose action). Creates feature branch with drafted changes.

Request Body

{
"component": "backend.scheduler",
"issue": "Polling every 60s is inefficient",
"proposed_solution": "Use priority queue with sleep-until-next"
}

Response

{
"branch": "morphee-suggests/scheduler-priority-queue-1739451234",
"files_changed": [
"backend/scheduler/runner.py",
"backend/tests/test_scheduler.py"
],
"pr_description": "## Summary\n\nOptimize task scheduler using priority queue...",
"review_url": "https://github.com/morphee-app/morphee-beta/compare/main...morphee-suggests/scheduler-priority-queue-1739451234",
"human_approval_required": true
}

Status Codes

  • 200 OK: Proposal created (requires user approval for execution)
  • 403 Forbidden: Self-improvement not enabled or user not authorized
  • 400 Bad Request: Invalid component or missing parameters

List Community Contributions

GET /api/morphee-self/contributions?status={status}

List community branches and Morphee's self-improvement proposals.

Query Parameters

  • status (optional): "proposed", "under_review", "approved", "rejected", "merged"

Response

{
"contributions": [
{
"id": "uuid",
"type": "community_pr",
"branch": "community/slack-integration",
"author": "sarah-dev",
"status": "under_review",
"morphee_recommendation": "approve",
"created_at": "2026-02-13T09:00:00Z"
},
{
"id": "uuid",
"type": "morphee_proposal",
"branch": "morphee-suggests/scheduler-priority-queue",
"component": "backend.scheduler",
"status": "proposed",
"created_at": "2026-02-13T10:00:00Z"
}
],
"total": 2
}

Status Codes

  • 200 OK: List retrieved
  • 403 Forbidden: User not authorized

Get Code Review History

GET /api/morphee-self/reviews/{branch_name}

Get Morphee's review history for a specific branch.

Response

{
"branch": "community/slack-integration",
"reviews": [
{
"id": "uuid",
"morphee_review": { /* full review object */ },
"morphee_recommendation": "approve",
"human_decision": "approved",
"human_reviewer_id": "sebastien-mathieu",
"reviewed_at": "2026-02-13T11:00:00Z",
"merged_at": "2026-02-13T11:30:00Z"
}
]
}

Status Codes

  • 200 OK: History retrieved
  • 404 Not Found: Branch not found

Partner API (V2.0+)

Status: PLANNED — Not Yet Implemented. These endpoints are designed but no backend routes exist yet.

Create API Key

POST /api/partners/keys

Create a new partner API key. Requires parent role.

Request Body

{
"partner_name": "Acme Corp",
"scopes": ["chat", "tasks", "memory"],
"rate_limit_per_hour": 10000
}

Response (201 Created)

{
"id": "uuid",
"api_key": "mk_live_a1b2c3d4e5f6...",
"partner_name": "Acme Corp",
"scopes": ["chat", "tasks", "memory"],
"rate_limit_per_hour": 10000,
"created_at": "2026-02-13T15:00:00Z"
}

Note: The api_key is only returned once. Store it securely.


Revoke API Key

DELETE /api/partners/keys/{key_id}

Revoke a partner API key. Requires parent role.

Response (200 OK)

{
"status": "revoked",
"message": "API key revoked successfully"
}

Get Usage Stats

GET /api/partners/usage

Get API usage statistics for partner keys.

Query Parameters

  • key_id (optional): Filter by specific API key
  • start_date (optional): Start date (ISO 8601)
  • end_date (optional): End date (ISO 8601)

Response (200 OK)

{
"usage": [
{
"date": "2026-02-13",
"requests": 8543,
"errors": 12,
"avg_latency_ms": 245
}
],
"current_hour_usage": 143,
"rate_limit": 10000
}

Get Revenue Dashboard

GET /api/partners/revenue

Get revenue dashboard data (extension sales, usage-based billing).

Response (200 OK)

{
"total_revenue": 45670.50,
"monthly_revenue": 3245.00,
"active_subscriptions": 127,
"extensions": [
{
"extension_id": "uuid",
"name": "Math Tutor",
"subscribers": 45,
"monthly_revenue": 1348.50,
"revenue_share_percent": 70
}
]
}

Embedding Endpoints (SDK — V2.0+)

Status: PLANNED — Not Yet Implemented. These endpoints are designed but no backend routes exist yet.

Chat Widget (iframe)

GET /embed/chat

Embeddable chat widget iframe. Auth via API key query param.

Query Parameters

  • apiKey: Partner API key (required)
  • theme: light or dark (optional)
  • groupId: User's Morphee group ID (required)

Response: HTML page with chat widget


Send Chat Message (SDK)

POST /api/embed/chat

Send a chat message via SDK. Auth via API key header.

Headers

Authorization: Bearer mk_live_...

Request Body

{
"conversation_id": "uuid",
"message": "What's on my calendar today?",
"group_id": "uuid"
}

Response (200 OK)

SSE stream (same format as /api/chat):

event: token
data: {"content": "You"}

event: token
data: {"content": " have"}

event: done
data: {"message_id": "uuid"}

Create Task (SDK)

POST /api/embed/tasks

Create a task via SDK. Auth via API key header.

Request Body

{
"title": "Follow up on customer inquiry",
"group_id": "uuid",
"space_id": "uuid",
"metadata": {
"customerId": "12345",
"source": "partner_app"
}
}

Response (201 Created)

{
"id": "uuid",
"title": "Follow up on customer inquiry",
"status": "pending",
"created_at": "2026-02-13T16:00:00Z"
}

Search Memory (SDK)

POST /api/embed/memory/search

Search memory via SDK. Auth via API key header.

Request Body

{
"query": "customer preferences for shipping",
"group_id": "uuid",
"space_id": "uuid",
"limit": 5
}

Response (200 OK)

{
"results": [
{
"content": "Customer prefers express shipping",
"score": 0.92,
"memory_type": "preference",
"created_at": "2026-02-10T10:00:00Z"
}
]
}

Analytics & Monitoring API (V2.0)

Status: PLANNED — Not Yet Implemented. These endpoints are designed but no backend routes exist yet.

Prometheus Metrics

GET /metrics

Public endpoint (no auth required) — Prometheus scraping endpoint.

Response (200 OK, text/plain)

# HELP morphee_api_requests_total Total API requests by endpoint and status
# TYPE morphee_api_requests_total counter
morphee_api_requests_total{endpoint="/api/chat",status="200"} 1523
morphee_api_requests_total{endpoint="/api/tasks",status="200"} 847

# HELP morphee_api_request_duration_seconds API request latency
# TYPE morphee_api_request_duration_seconds histogram
morphee_api_request_duration_seconds_bucket{endpoint="/api/chat",le="0.1"} 1200
morphee_api_request_duration_seconds_bucket{endpoint="/api/chat",le="0.5"} 1500

# HELP morphee_llm_tokens_total Total LLM tokens consumed
# TYPE morphee_llm_tokens_total counter
morphee_llm_tokens_total{model="claude-sonnet-4-5",type="input"} 250000
morphee_llm_tokens_total{model="claude-sonnet-4-5",type="output"} 180000

# HELP morphee_memory_rag_latency_seconds RAG pipeline latency
# TYPE morphee_memory_rag_latency_seconds histogram
morphee_memory_rag_latency_seconds_bucket{le="0.05"} 450

PostHog Event Capture

PostHog events are captured server-side via the Python backend, not exposed as a public API. Events are sent asynchronously to PostHog cloud or self-hosted instance.

Privacy: All event capture is opt-in via PUT /api/settings/privacy/analytics_enabled. PII (message content, emails, names) is never sent — only opaque IDs and metadata.


Interactive API Documentation

FastAPI provides auto-generated interactive documentation:

Swagger UI: http://localhost:8000/docs ReDoc: http://localhost:8000/redoc


Security Best Practices

Token Management

  1. Store tokens securely — Use httpOnly cookies or secure storage
  2. Refresh tokens before expiry — Implement automatic token refresh
  3. Never expose tokens — Don't log or include in URLs (except WebSocket query param)
  4. Handle token expiry — Gracefully handle 401 responses

WebSocket Security

  1. Token in query parameter — Only way to authenticate WebSocket in browser
  2. Connection validation — Invalid tokens rejected immediately
  3. Group isolation enforced — Backend filters all events by group
  4. Close on auth failure — Connection closed with policy violation code

Last Updated: February 22, 2026

For more information, see: