Files
carry-your-live/API_SPEC.md
T
2026-08-09 21:42:54 +02:00

13 KiB

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)

{
  "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

{
  "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)

{
  "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)

{
  "categories": [
    {
      "id": "cat_1",
      "name": "Work",
      "color": "#E53935",
      "order": 0,
      "createdAt": 1699000000000,
      "updatedAt": 1699000000000
    }
  ]
}

Create Category

POST /categories

Request Body

{
  "name": "New Category",
  "color": "#1E88E5",
  "order": 6
}

Response (201 Created)

{
  "id": "cat_new",
  "name": "New Category",
  "color": "#1E88E5",
  "order": 6,
  "createdAt": 1699150000000,
  "updatedAt": 1699150000000
}

Update Category

PATCH /categories/{id}

Request Body

{
  "name": "Updated Name",
  "color": "#FB8C00",
  "order": 1
}

Response (200 OK)

{
  "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)

{
  "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)

{
  "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

{
  "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)

{
  "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)

{
  "title": "Updated Title",
  "description": "Updated description",
  "categoryId": "cat_2",
  "priority": "high",
  "completed": true,
  "dueDate": 1699300000000,
  "dueTime": "12:00"
}

Response (200 OK)

{
  "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

{
  "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)

{
  "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)

{
  "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

{
  "title": "New Subtask",
  "order": 2
}

Response (201 Created)

{
  "id": "sub_new",
  "taskId": "task_1",
  "title": "New Subtask",
  "completed": false,
  "order": 2,
  "createdAt": 1699150000000,
  "updatedAt": 1699150000000
}

Update Subtask

PATCH /subtasks/{id}

Request Body

{
  "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)

{
  "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

{
  "darkMode": true,
  "notifications": false,
  "reminderTime": "08:00",
  "defaultCategory": "cat_2",
  "sortBy": "priority",
  "sortOrder": "desc"
}

Response (200 OK)


Data Types

Category

interface Category {
  id: string;
  name: string;
  color: string;      // HEX format
  order: number;
  createdAt: number;  // Unix timestamp (ms)
  updatedAt: number;  // Unix timestamp (ms)
}

Task

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

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

interface Tombstone {
  entity: 'categories' | 'tasks' | 'subtasks' | 'repeatProfiles' | 'friendships';
  id: string;        // id of the deleted row
  updatedAt: number; // Unix timestamp (ms) of the deletion
}

UserSettings

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:

{
  "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:

{
  "event": "data_changed",
  "entities": ["tasks", "categories"],
  "timestamp": 1699150000000
}

Client should trigger sync on receipt.