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:
- Sign up →
/api/auth/signup→ Receiveaccess_tokenandrefresh_token - Sign in →
/api/auth/signin→ Receiveaccess_tokenandrefresh_token - Use access_token in
Authorization: Bearer <token>header for protected endpoints - 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 request201 Created: Successful POST request204 No Content: Successful DELETE request400 Bad Request: Invalid request data404 Not Found: Resource not found422 Unprocessable Entity: Validation error500 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 healthy503 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 reachablefalse: Service is unhealthy or unreachablenull: Service is not configured (e.g., Redis not enabled)
Status Codes
200 OK: All critical services are healthy503 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 successfully400 Bad Request: User already exists, invalid data, or minor without parent email422 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 successful401 Unauthorized: Invalid credentials422 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 successfully401 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 successful401 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 retrieved401 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 nameavatar_url(string): URL to avatar image
Response (200 OK)
Returns the updated user profile object.
Status Codes
200 OK: Profile updated401 Unauthorized: Invalid or missing token422 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 passwordnew_password(string): The new password (minimum 6 characters)
Response (200 OK)
{
"status": "password_changed",
"message": "Password changed successfully"
}
Status Codes
200 OK: Password changed400 Bad Request: Current password is incorrect401 Unauthorized: Invalid or missing token422 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 datamarkdown: 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 metadataprofile.json— User profilegroup.json— Group membershipconversations/*.md— Human-readable conversation filestasks.json,spaces.json,memories.json, etc.
Status Codes
200 OK: Export generated successfully400 Bad Request: Invalid format parameter401 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 deleted400 Bad Request: Confirmation text doesn't match or deletion failed401 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"
}
]
Grant Consent
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"
}
Withdraw Consent
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 generated400 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
- Frontend calls
GET /api/auth/providersto discover enabled providers - User clicks "Sign in with Google" → frontend calls
GET /api/auth/sso/google - Frontend redirects to
authorize_url(full-page redirect) - User authenticates with provider → GoTrue redirects to
/auth/callback#access_token=...&refresh_token=... - Frontend extracts tokens from URL fragment → calls
POST /api/auth/sso/callback - Backend verifies token, finds or creates morphee_user, returns
AuthResponse - Frontend saves auth, fetches
/api/auth/me, redirects to/or/onboarding
Verify Parental Consent
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 verified400 Bad Request: Invalid or expired token
Resend Parental Consent Email
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 resent400 Bad Request: User is not a minor or no parent email on file401 Unauthorized: Not authenticated
Check Parental Consent Status
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 returned401 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 successfully400 Bad Request: User already in a group or invalid data401 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 retrieved403 Forbidden: User not in a group404 Not Found: Group not found401 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 updated400 Bad Request: Invalid data403 Forbidden: Not an admin or not in group401 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 deleted403 Forbidden: Not an admin or not in group404 Not Found: Group not found401 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 inviterole(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 created400 Bad Request: Active invite exists, user already a member, or not a parent401 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 cancelled403 Forbidden: Not a parent404 Not Found: Invite not found or already processed
Create Invite Link
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 group400 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 removed400 Bad Request: Not a parent or cannot remove last parent404 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 descriptionparent_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 successfully401 Unauthorized: Missing or invalid token403 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 successfully401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 namedescription(string, max 2000 chars): Space description
Response (200 OK)
Returns the updated Space object.
Status Codes
200 OK: Space updated successfully400 Bad Request: Invalid request data401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 successfully401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 UUIDstatus(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 successfully401 Unauthorized: Missing or invalid token403 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 successfully401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 totype(string): Task type identifierdescription(string): Description of the taskinput(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 descriptionstatus(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 successfully400 Bad Request: Invalid request data401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 successfully401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 successfully400 Bad Request: Task cannot be paused (invalid state)401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 successfully400 Bad Request: Task cannot be resumed (invalid state)401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 successfully400 Bad Request: Task cannot be cancelled (invalid state)401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 found400 Bad Request: Interface is not configurable (noconfig_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 integrationaction_name(string): Name of the action to executeuser_id(string): User performing the actiongroup_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 exist422 Unprocessable Entity: Missing required fields500 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:
- Valid JWT access token (from
/api/auth/signinor/api/auth/signup) - User must be a member of a group
- 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 grouptask.updated: Task metadata updatedtask.status_changed: Task status changedtask.deleted: Task deleted
Action Events
action.executed: Integration action completed successfullyaction.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 processingrunning: Task is currently being processedcompleted: Task finished successfullyfailed: Task encountered an errorcancelled: Task was cancelledpaused: 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 analysisname(string, max 255 chars): File nametype(string): MIME type —image/jpeg,image/png,image/gif, orimage/webpdata(string): Base64-encoded image data (with or withoutdata: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
| Event | Data | Description |
|---|---|---|
token | string | A text chunk of the AI response |
tool_use | JSON object | LLM is calling a tool: {id, tool, input} |
tool_result | JSON object | Tool execution result: {id, tool, success, result?, error?} |
approval_request | JSON object | Action requires user approval: {approval_id, id, tool, input} |
title | string | Auto-generated conversation title (new conversations only) |
done | JSON object | Completion signal with conversation_id and message_id |
error | JSON object | Error 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 successfully401 Unauthorized: Missing or invalid token403 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 retrieved401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 titleis_pinned(boolean): Pin conversation to top of listis_archived(boolean): Archive conversation (hidden from default list)
Response (200 OK)
Returns the updated conversation object.
Status Codes
200 OK: Conversation updated401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 deleted401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 theapproval_requestSSE 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 resumes401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 Not Found: Approval request not found or expired (120s timeout)
Approval Flow
- During a chat stream, the AI may call a tool with
ai_access: "propose"(e.g.,webhook__send) - The SSE stream emits an
approval_requestevent with{approval_id, id, tool, input} - Frontend displays an approval UI to the user
- User clicks Approve or Reject →
POST /api/chat/approve/{approval_id} - 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 withcomponent_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 successfully400 Bad Request: Invalid tier or missing fields401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group
Event Tiers
| Tier | Transport | Description |
|---|---|---|
local | None (frontend only) | Handled entirely in frontend (dismiss, collapse). Should not reach this endpoint. |
ai_turn | SSE stream | Injected 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 conversationmessage_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 updated401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group or message is not a user message404 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 started401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 conversationmessage_id(required): UUID of the message to pin
Response (200 OK)
Returns the updated Message object with pinned: true.
Status Codes
200 OK: Message pinned400 Bad Request: Pin limit reached (max 5) or message not found401 Unauthorized: Missing or invalid token403 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 conversationmessage_id(required): UUID of the message to unpin
Response (200 OK)
Returns the updated Message object with pinned: false.
Status Codes
200 OK: Message unpinned401 Unauthorized: Missing or invalid token403 Forbidden: User not in a group404 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 token403 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 hasrole("user" or "assistant") andcontent(string). Defaults to empty array.
Response (200 OK, text/event-stream)
Same SSE event types as the Chat API:
| Event | Data | Description |
|---|---|---|
token | string | A text chunk of the AI response |
tool_use | JSON object | LLM is calling a tool: {id, tool, input} |
tool_result | JSON object | Tool execution result: {id, tool, success, result?, error?} |
done | JSON object | Completion signal with stop_reason |
error | JSON object | Error 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 grouponboarding__complete— Signals onboarding is complete (returns_onboarding_complete: true)
Status Codes
200 OK: Stream started successfully400 Bad Request: User already has a group401 Unauthorized: Missing or invalid token422 Unprocessable Entity: Missing required fields
Onboarding Flow
- User signs up →
group_idis null → frontend redirects to/onboarding - Frontend sends messages to
POST /api/onboarding/chatwith full history - AI detects persona, asks for group name, creates group + spaces via tools
onboarding__completetool returns{_onboarding_complete: true}- Frontend detects completion, calls
GET /api/auth/meto refresh user data - 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 read404 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 token422 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 generated400 Bad Request: Invalid redirect URI scheme501 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 Googlestate(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 retrieved404 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 interface400 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 updated400 Bad Request: Invalid definition404 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 deleted404 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.
Global Search
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 UUIDtype(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 parameter401 Unauthorized: Missing or invalid token403 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 ofspace,conversation,task,integration,action,memoryresource_id(string): UUID of the resourceprincipal_id(UUID): User receiving the permissionoperation(string): One ofread,write,delete,execute,*permission(string):allowordeny
Response (200 OK)
Returns the created ResourceACL object.
Status Codes
200 OK: Permission granted403 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 typeresource_id(required): Resource UUIDprincipal_id(required): User UUIDoperation(required): Operation to revoke
Response (200 OK)
{ "success": true }
Status Codes
200 OK: Permission revoked403 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 typeresource_id(required): Resource UUIDoperation(required): Operation to check
Response (200 OK)
{
"allowed": true,
"reason": "Owner of resource",
"source": "owner"
}
Status Codes
200 OK: Check result returned403 Forbidden: Not in a group
List ACLs
GET /api/acl/list
List all ACL entries for a resource.
Query Parameters
resource_type(required): Resource typeresource_id(required): Resource UUID
Response (200 OK)
Returns array of ResourceACL objects.
Status Codes
200 OK: ACLs returned403 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, ornoneai_personality_mode(string):child_friendly,professional,educational, orstandardscreen_time_limit_mins(integer): Daily screen time limit in minutesquiet_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/updated400 Bad Request: Cannot monitor yourself403 Forbidden: Not in a group
Consent to Monitoring
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 updated400 Bad Request: Cannot monitor yourself403 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:
| Field | Type | Values | Description |
|---|---|---|---|
principal | string | UUID or email | User identified by UUID or email address |
operation | string | read, write, delete, execute, * | Operation to control. * matches all operations |
permission | string | allow, deny | Whether to grant or deny access |
Resolution order (first match wins):
- Walk from the resource's directory up to the repository root
- At each level, check for a
.aclfile - Within each file, match by
principal(UUID or email) andoperation(exact or wildcard) - If no
.aclfile 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 querytime_range(optional, object): Filter by commit timestampbranch(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 fromtarget(required, string): Branch to merge intostrategy(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 updated403 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 querycategory(optional): Filter by categorylimit(optional, default 50, max 100): Results per pageoffset(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 installed400 Bad Request: Invalid manifest or permissions503 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 (checksuccessfield)400 Bad Request: No group found503 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 providerstate(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 completed403 Forbidden: Self-awareness not enabled400 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 generated403 Forbidden: Self-awareness not enabled404 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 retrieved403 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 authorized404 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 authorized400 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 retrieved403 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 retrieved404 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 keystart_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:lightordark(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
- Store tokens securely — Use httpOnly cookies or secure storage
- Refresh tokens before expiry — Implement automatic token refresh
- Never expose tokens — Don't log or include in URLs (except WebSocket query param)
- Handle token expiry — Gracefully handle 401 responses
WebSocket Security
- Token in query parameter — Only way to authenticate WebSocket in browser
- Connection validation — Invalid tokens rejected immediately
- Group isolation enforced — Backend filters all events by group
- Close on auth failure — Connection closed with policy violation code
Last Updated: February 22, 2026
For more information, see:
- README.md - Getting started
- architecture.md - System architecture
- interfaces.md - Integration/Interface system guide
- testing.md - Testing guide