Files
2026-08-09 21:42:54 +02:00

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.