701 lines
13 KiB
Markdown
701 lines
13 KiB
Markdown
# API Specification
|
|
|
|
## Base URL
|
|
```
|
|
Development: http://localhost:3000/api
|
|
Production: https://api.carryyourlive.com/api
|
|
```
|
|
|
|
## Authentication
|
|
All endpoints require authentication via Bearer token:
|
|
```
|
|
Authorization: Bearer <jwt_token>
|
|
```
|
|
|
|
## Endpoints
|
|
|
|
### Sync
|
|
|
|
#### Pull Changes
|
|
```
|
|
GET /sync
|
|
```
|
|
|
|
**Query Parameters**
|
|
| Parameter | Type | Required | Description |
|
|
|-----------|------|----------|-------------|
|
|
| since | integer | Yes | Unix timestamp (ms) of last successful sync |
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"categories": [
|
|
{
|
|
"id": "cat_1",
|
|
"name": "Work",
|
|
"color": "#E53935",
|
|
"order": 0,
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699000000000
|
|
}
|
|
],
|
|
"tasks": [
|
|
{
|
|
"id": "task_1",
|
|
"title": "Finish project",
|
|
"description": "Complete the mobile app",
|
|
"categoryId": "cat_1",
|
|
"priority": "high",
|
|
"completed": false,
|
|
"dueDate": 1699100000000,
|
|
"dueTime": "17:00",
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699000000000
|
|
}
|
|
],
|
|
"subtasks": [
|
|
{
|
|
"id": "sub_1",
|
|
"taskId": "task_1",
|
|
"parentSubtaskId": null,
|
|
"title": "Setup Expo project",
|
|
"completed": true,
|
|
"order": 0,
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699000000000
|
|
}
|
|
],
|
|
"repeatProfiles": [],
|
|
"friendships": [],
|
|
"deleted": [
|
|
{
|
|
"entity": "tasks",
|
|
"id": "task_5",
|
|
"updatedAt": 1699150000000
|
|
}
|
|
],
|
|
"timestamp": 1699150000000
|
|
}
|
|
```
|
|
|
|
`deleted` contains every row tombstoned (deleted) since `since` — one entry per entity type/id. Clients must remove the row locally (and any related rows: a task deletion implies its subtasks, a category deletion implies its tasks and their subtasks, a parent subtask deletion implies its children).
|
|
|
|
**Error Responses**
|
|
| Status | Code | Message |
|
|
|--------|------|---------|
|
|
| 400 | INVALID_TIMESTAMP | Invalid or missing `since` parameter |
|
|
| 401 | UNAUTHORIZED | Missing or invalid token |
|
|
| 500 | SERVER_ERROR | Internal server error |
|
|
|
|
---
|
|
|
|
#### Push Changes
|
|
```
|
|
POST /sync/push
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"changes": {
|
|
"categories": [
|
|
{
|
|
"id": "cat_new",
|
|
"name": "New Category",
|
|
"color": "#1E88E5",
|
|
"order": 6,
|
|
"createdAt": 1699150000000,
|
|
"updatedAt": 1699150000000
|
|
}
|
|
],
|
|
"tasks": [
|
|
{
|
|
"id": "task_new",
|
|
"title": "New Task",
|
|
"description": "Task description",
|
|
"categoryId": "cat_1",
|
|
"priority": "medium",
|
|
"completed": false,
|
|
"dueDate": 1699200000000,
|
|
"dueTime": "10:00",
|
|
"createdAt": 1699150000000,
|
|
"updatedAt": 1699150000000
|
|
}
|
|
],
|
|
"subtasks": [
|
|
{
|
|
"id": "sub_new",
|
|
"taskId": "task_new",
|
|
"parentSubtaskId": null,
|
|
"title": "Subtask 1",
|
|
"completed": false,
|
|
"order": 0,
|
|
"createdAt": 1699150000000,
|
|
"updatedAt": 1699150000000
|
|
}
|
|
]
|
|
},
|
|
"deleted": [
|
|
{
|
|
"entity": "categories",
|
|
"id": "cat_gone",
|
|
"updatedAt": 1699150000000
|
|
}
|
|
],
|
|
"lastPulledAt": 1699100000000
|
|
}
|
|
```
|
|
|
|
`deleted` is a list of tombstones the client recorded locally since its last successful sync. Deletions are resolved by last-writer-wins: if the server row's `updatedAt` is newer than the tombstone's `updatedAt`, the deletion is rejected and reported as a `server_wins` conflict (with the server row as `serverVersion`) so the client re-pulls it. Accepted deletions remove the rows server-side; cascaded rows (subtasks of a deleted task, tasks/subtasks of a deleted category, children of a deleted parent subtask) are tombstoned automatically so every device removes them.
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"success": true,
|
|
"timestamp": 1699150000000,
|
|
"conflicts": [
|
|
{
|
|
"entity": "tasks",
|
|
"id": "task_1",
|
|
"serverVersion": { ... },
|
|
"clientVersion": { ... },
|
|
"resolution": "server_wins"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Error Responses**
|
|
| Status | Code | Message |
|
|
|--------|------|---------|
|
|
| 400 | INVALID_PAYLOAD | Malformed request body |
|
|
| 401 | UNAUTHORIZED | Missing or invalid token |
|
|
| 409 | CONFLICT | Unresolvable conflicts (if not using auto-resolution) |
|
|
| 500 | SERVER_ERROR | Internal server error |
|
|
|
|
---
|
|
|
|
### Categories
|
|
|
|
#### List Categories
|
|
```
|
|
GET /categories
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"categories": [
|
|
{
|
|
"id": "cat_1",
|
|
"name": "Work",
|
|
"color": "#E53935",
|
|
"order": 0,
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699000000000
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Create Category
|
|
```
|
|
POST /categories
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"name": "New Category",
|
|
"color": "#1E88E5",
|
|
"order": 6
|
|
}
|
|
```
|
|
|
|
**Response** (201 Created)
|
|
```json
|
|
{
|
|
"id": "cat_new",
|
|
"name": "New Category",
|
|
"color": "#1E88E5",
|
|
"order": 6,
|
|
"createdAt": 1699150000000,
|
|
"updatedAt": 1699150000000
|
|
}
|
|
```
|
|
|
|
#### Update Category
|
|
```
|
|
PATCH /categories/{id}
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"name": "Updated Name",
|
|
"color": "#FB8C00",
|
|
"order": 1
|
|
}
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"id": "cat_1",
|
|
"name": "Updated Name",
|
|
"color": "#FB8C00",
|
|
"order": 1,
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699150000000
|
|
}
|
|
```
|
|
|
|
#### Delete Category
|
|
```
|
|
DELETE /categories/{id}
|
|
```
|
|
|
|
**Response** (204 No Content)
|
|
|
|
---
|
|
|
|
### Tasks
|
|
|
|
#### List Tasks
|
|
```
|
|
GET /tasks
|
|
```
|
|
|
|
**Query Parameters**
|
|
| Parameter | Type | Description |
|
|
|-----------|------|-------------|
|
|
| categoryId | string | Filter by category |
|
|
| completed | boolean | Filter by completion status |
|
|
| dueBefore | integer | Due date before timestamp (ms) |
|
|
| dueAfter | integer | Due date after timestamp (ms) |
|
|
| priority | string | Filter by priority |
|
|
| sortBy | string | Sort field: dueDate, priority, createdAt, title |
|
|
| sortOrder | string | asc or desc |
|
|
| limit | integer | Pagination limit (default 50) |
|
|
| offset | integer | Pagination offset |
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"tasks": [
|
|
{
|
|
"id": "task_1",
|
|
"title": "Finish project",
|
|
"description": "Complete the mobile app",
|
|
"categoryId": "cat_1",
|
|
"priority": "high",
|
|
"completed": false,
|
|
"dueDate": 1699100000000,
|
|
"dueTime": "17:00",
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699000000000,
|
|
"subtasks": [
|
|
{
|
|
"id": "sub_1",
|
|
"title": "Setup Expo project",
|
|
"completed": true,
|
|
"order": 0
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"total": 1,
|
|
"limit": 50,
|
|
"offset": 0
|
|
}
|
|
```
|
|
|
|
#### Get Task
|
|
```
|
|
GET /tasks/{id}
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"id": "task_1",
|
|
"title": "Finish project",
|
|
"description": "Complete the mobile app",
|
|
"categoryId": "cat_1",
|
|
"priority": "high",
|
|
"completed": false,
|
|
"dueDate": 1699100000000,
|
|
"dueTime": "17:00",
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699000000000,
|
|
"subtasks": [...]
|
|
}
|
|
```
|
|
|
|
#### Create Task
|
|
```
|
|
POST /tasks
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"title": "New Task",
|
|
"description": "Task description",
|
|
"categoryId": "cat_1",
|
|
"priority": "medium",
|
|
"dueDate": 1699200000000,
|
|
"dueTime": "10:00",
|
|
"subtasks": [
|
|
{ "title": "Subtask 1" },
|
|
{ "title": "Subtask 2" }
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response** (201 Created)
|
|
```json
|
|
{
|
|
"id": "task_new",
|
|
"title": "New Task",
|
|
"description": "Task description",
|
|
"categoryId": "cat_1",
|
|
"priority": "medium",
|
|
"completed": false,
|
|
"dueDate": 1699200000000,
|
|
"dueTime": "10:00",
|
|
"createdAt": 1699150000000,
|
|
"updatedAt": 1699150000000,
|
|
"subtasks": [
|
|
{ "id": "sub_1", "title": "Subtask 1", "completed": false, "order": 0 },
|
|
{ "id": "sub_2", "title": "Subtask 2", "completed": false, "order": 1 }
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Update Task
|
|
```
|
|
PATCH /tasks/{id}
|
|
```
|
|
|
|
**Request Body** (all fields optional)
|
|
```json
|
|
{
|
|
"title": "Updated Title",
|
|
"description": "Updated description",
|
|
"categoryId": "cat_2",
|
|
"priority": "high",
|
|
"completed": true,
|
|
"dueDate": 1699300000000,
|
|
"dueTime": "12:00"
|
|
}
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"id": "task_1",
|
|
"title": "Updated Title",
|
|
"description": "Updated description",
|
|
"categoryId": "cat_2",
|
|
"priority": "high",
|
|
"completed": true,
|
|
"dueDate": 1699300000000,
|
|
"dueTime": "12:00",
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699150000000,
|
|
"subtasks": [...]
|
|
}
|
|
```
|
|
|
|
#### Delete Task
|
|
```
|
|
DELETE /tasks/{id}
|
|
```
|
|
|
|
**Response** (204 No Content)
|
|
|
|
#### Batch Update Tasks
|
|
```
|
|
POST /tasks/batch
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"operations": [
|
|
{ "type": "update", "id": "task_1", "data": { "completed": true } },
|
|
{ "type": "delete", "id": "task_2" },
|
|
{ "type": "create", "data": { "title": "New Task", "categoryId": "cat_1" } }
|
|
]
|
|
}
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"results": [
|
|
{ "id": "task_1", "success": true },
|
|
{ "id": "task_2", "success": true },
|
|
{ "id": "task_new", "success": true, "data": { ... } }
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Subtasks
|
|
|
|
#### List Subtasks
|
|
```
|
|
GET /tasks/{taskId}/subtasks
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"subtasks": [
|
|
{
|
|
"id": "sub_1",
|
|
"taskId": "task_1",
|
|
"title": "Subtask 1",
|
|
"completed": false,
|
|
"order": 0,
|
|
"createdAt": 1699000000000,
|
|
"updatedAt": 1699000000000
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### Create Subtask
|
|
```
|
|
POST /tasks/{taskId}/subtasks
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"title": "New Subtask",
|
|
"order": 2
|
|
}
|
|
```
|
|
|
|
**Response** (201 Created)
|
|
```json
|
|
{
|
|
"id": "sub_new",
|
|
"taskId": "task_1",
|
|
"title": "New Subtask",
|
|
"completed": false,
|
|
"order": 2,
|
|
"createdAt": 1699150000000,
|
|
"updatedAt": 1699150000000
|
|
}
|
|
```
|
|
|
|
#### Update Subtask
|
|
```
|
|
PATCH /subtasks/{id}
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"title": "Updated Subtask",
|
|
"completed": true,
|
|
"order": 1
|
|
}
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
|
|
#### Delete Subtask
|
|
```
|
|
DELETE /subtasks/{id}
|
|
```
|
|
|
|
**Response** (204 No Content)
|
|
|
|
---
|
|
|
|
### Users
|
|
|
|
#### Get Current User
|
|
```
|
|
GET /users/me
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
```json
|
|
{
|
|
"id": "user_1",
|
|
"email": "user@example.com",
|
|
"name": "John Doe",
|
|
"settings": {
|
|
"darkMode": false,
|
|
"notifications": true,
|
|
"reminderTime": "09:00",
|
|
"defaultCategory": "cat_1",
|
|
"sortBy": "dueDate",
|
|
"sortOrder": "asc"
|
|
},
|
|
"createdAt": 1699000000000
|
|
}
|
|
```
|
|
|
|
#### Update Settings
|
|
```
|
|
PATCH /users/me/settings
|
|
```
|
|
|
|
**Request Body**
|
|
```json
|
|
{
|
|
"darkMode": true,
|
|
"notifications": false,
|
|
"reminderTime": "08:00",
|
|
"defaultCategory": "cat_2",
|
|
"sortBy": "priority",
|
|
"sortOrder": "desc"
|
|
}
|
|
```
|
|
|
|
**Response** (200 OK)
|
|
|
|
---
|
|
|
|
## Data Types
|
|
|
|
### Category
|
|
```typescript
|
|
interface Category {
|
|
id: string;
|
|
name: string;
|
|
color: string; // HEX format
|
|
order: number;
|
|
createdAt: number; // Unix timestamp (ms)
|
|
updatedAt: number; // Unix timestamp (ms)
|
|
}
|
|
```
|
|
|
|
### Task
|
|
```typescript
|
|
interface Task {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
categoryId: string;
|
|
priority: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
|
completed: boolean;
|
|
dueDate: number; // Unix timestamp (ms), 0 if not set
|
|
dueTime: string; // HH:MM format, empty if not set
|
|
endTime: string; // HH:MM format, empty if not set
|
|
allDay: boolean;
|
|
repeat: 'none' | 'daily' | 'weekly' | 'monthly' | 'custom';
|
|
repeatInterval: number;
|
|
repeatDays: string; // comma separated weekday numbers, e.g. "0,2,4"
|
|
seriesId: string; // repeating-series id, empty if not part of a series
|
|
reminder: 'none' | 'at_time' | '15' | '30' | '60' | '120' | '1440';
|
|
reminders: string; // comma separated reminder values, empty if none
|
|
assigneeId: string | null;
|
|
completedAt: number | null; // Unix timestamp (ms) when completed
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
subtasks?: Subtask[];
|
|
}
|
|
```
|
|
|
|
### Subtask
|
|
```typescript
|
|
interface Subtask {
|
|
id: string;
|
|
taskId: string;
|
|
parentSubtaskId: string | null; // id of the parent subtask, null for top-level
|
|
title: string;
|
|
description: string;
|
|
priority: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
|
completed: boolean;
|
|
dueDate: number;
|
|
dueTime: string;
|
|
endTime: string;
|
|
allDay: boolean;
|
|
repeat: 'none' | 'daily' | 'weekly' | 'monthly' | 'custom';
|
|
repeatInterval: number;
|
|
repeatDays: string;
|
|
seriesId: string;
|
|
reminder: 'none' | 'at_time' | '15' | '30' | '60' | '120' | '1440';
|
|
reminders: string;
|
|
assigneeId: string | null;
|
|
order: number;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
```
|
|
|
|
### Tombstone
|
|
```typescript
|
|
interface Tombstone {
|
|
entity: 'categories' | 'tasks' | 'subtasks' | 'repeatProfiles' | 'friendships';
|
|
id: string; // id of the deleted row
|
|
updatedAt: number; // Unix timestamp (ms) of the deletion
|
|
}
|
|
```
|
|
|
|
### UserSettings
|
|
```typescript
|
|
interface UserSettings {
|
|
darkMode: boolean;
|
|
notifications: boolean;
|
|
reminderTime: string; // HH:MM
|
|
defaultCategory: string; // Category ID
|
|
sortBy: 'dueDate' | 'priority' | 'title' | 'createdAt';
|
|
sortOrder: 'asc' | 'desc';
|
|
}
|
|
```
|
|
|
|
## Error Format
|
|
|
|
All error responses follow this format:
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "ERROR_CODE",
|
|
"message": "Human readable message",
|
|
"details": {}
|
|
}
|
|
}
|
|
```
|
|
|
|
Common error codes:
|
|
- `INVALID_PAYLOAD` - Request validation failed
|
|
- `UNAUTHORIZED` - Authentication required
|
|
- `FORBIDDEN` - Insufficient permissions
|
|
- `NOT_FOUND` - Resource not found
|
|
- `CONFLICT` - Version conflict
|
|
- `RATE_LIMITED` - Too many requests
|
|
- `SERVER_ERROR` - Internal server error
|
|
|
|
## Rate Limiting
|
|
|
|
- 100 requests/minute per user for sync endpoints
|
|
- 500 requests/minute per user for other endpoints
|
|
- Returns 429 with `Retry-After` header
|
|
|
|
## Webhooks (Optional)
|
|
|
|
Server can notify clients of remote changes:
|
|
```
|
|
POST /webhooks/sync
|
|
```
|
|
|
|
Payload:
|
|
```json
|
|
{
|
|
"event": "data_changed",
|
|
"entities": ["tasks", "categories"],
|
|
"timestamp": 1699150000000
|
|
}
|
|
```
|
|
|
|
Client should trigger sync on receipt. |