{"basePath":"/v1","definitions":{"apierr.ErrorCode":{"enum":[1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,2001,2002,3001,3002,3003,3004,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,5001,5002,5003,5004,5005,5006,6001,7001,9001,9002,9003],"type":"integer"},"apierr.ProblemDetail":{"properties":{"detail":{"example":"Missing or invalid JWT claims","type":"string"},"error_code":{"allOf":[{"$ref":"#/definitions/apierr.ErrorCode"}],"example":1001},"errors":{"items":{"$ref":"#/definitions/apierr.ProblemDetail"},"type":"array"},"instance":{"type":"string"},"status":{"example":401,"type":"integer"},"title":{"example":"Unauthorized","type":"string"},"type":{"example":"about:blank","type":"string"}},"type":"object"},"handlers.ActiveTimerStateResponse":{"properties":{"elapsed_ms":{"type":"integer"},"focus_level":{"type":"integer"},"is_break":{"type":"boolean"},"mode":{"type":"string"},"originating_device":{"type":"string"},"started_at":{"type":"string"},"status":{"type":"string"},"task_id":{"type":"string"},"timer_algorithm":{"type":"string"},"timer_id":{"type":"string"},"updated_at":{"type":"string"},"user_id":{"type":"string"}},"type":"object"},"handlers.ActivityResponse":{"properties":{"color":{"type":"string"},"created_at":{"type":"string"},"deleted_at":{"type":"string"},"icon":{"type":"string"},"id":{"type":"string"},"is_builtin":{"type":"boolean"},"name":{"type":"string"},"updated_at":{"type":"string"},"user_id":{"type":"string"}},"type":"object"},"handlers.BatchErrorEntry":{"properties":{"code":{"example":"entity_not_found","type":"string"},"trigger":{"example":"Targeted entity does not exist or is soft-deleted","type":"string"}},"type":"object"},"handlers.DailySnapshotResponse":{"properties":{"achieved":{"type":"boolean"},"achieved_at":{"type":"string"},"date":{"type":"string"},"goal_at_time":{"type":"integer"},"sessions_completed":{"type":"integer"},"updated_at":{"type":"string"}},"type":"object"},"handlers.DecisionPromptResponse":{"properties":{"key":{"example":"reasons","type":"string"},"phase":{"description":"Phase 1 is answered right after deciding; the first non-empty phase 2\nanswer marks the decision reviewed.","enum":[1,2],"example":1,"type":"integer"},"prompt":{"example":"Why do you think this is a good idea?","type":"string"}},"type":"object"},"handlers.DecisionResponse":{"properties":{"answers":{"additionalProperties":{"type":"string"},"description":"Answers maps a prompt key to the user's text. Known keys and their\nphase come from GET /decisions/prompts; unknown keys are kept as-is.","example":{"outcome":"Went well","reasons":"Better pay and a team I trust"},"type":"object"},"created_at":{"type":"string"},"decided_at":{"type":"string"},"deleted_at":{"description":"DeletedAt is set only on a sync tombstone.","type":"string"},"edited_after_review_at":{"type":"string"},"id":{"type":"string"},"review_task_id":{"type":"string"},"reviewed_at":{"type":"string"},"same_call":{"enum":["yes","no","unsure"],"type":"string"},"title":{"type":"string"},"updated_at":{"type":"string"},"user_id":{"type":"string"}},"type":"object"},"handlers.ErrorCodeEntry":{"properties":{"alternate_http_statuses":{"description":"AlternateHTTPStatuses lists statuses an older endpoint also pairs with this\ncode. Kept rather than fixed because clients already branch on the pairing;\nnew endpoints use HTTPStatus only.","example":[422],"items":{"type":"integer"},"type":"array"},"code":{"allOf":[{"$ref":"#/definitions/apierr.ErrorCode"}],"example":1001},"domain":{"example":"auth","type":"string"},"http_status":{"example":401,"type":"integer"},"reserved":{"description":"Reserved codes are defined but no endpoint returns them today.","type":"boolean"},"title":{"example":"Unauthorized","type":"string"},"trigger":{"example":"JWT present but claims extraction fails","type":"string"}},"type":"object"},"handlers.ErrorCodesResponse":{"properties":{"batch_codes":{"items":{"$ref":"#/definitions/handlers.BatchErrorEntry"},"type":"array"},"codes":{"items":{"$ref":"#/definitions/handlers.ErrorCodeEntry"},"type":"array"},"endpoints":{"additionalProperties":{"items":{"type":"integer"},"type":"array"},"type":"object"}},"type":"object"},"handlers.GoalEntryResponse":{"properties":{"carried_from":{"type":"string"},"created_at":{"type":"string"},"done":{"type":"boolean"},"horizon_goal_id":{"type":"string"},"id":{"type":"string"},"position":{"type":"integer"},"text":{"type":"string"},"updated_at":{"type":"string"}},"type":"object"},"handlers.HorizonGoalResponse":{"properties":{"carried_from":{"type":"string"},"created_at":{"type":"string"},"done_count":{"description":"DoneCount / EntryCount are the \"N of M done\" tally. Sent rather than\nleft to the client to compute so every surface shows the same number.","type":"integer"},"entries":{"description":"Entries are the period's intentions, in the user's own order. Always\npresent, empty for a period with none, so a client never has to\ndistinguish absent from empty.","items":{"$ref":"#/definitions/handlers.GoalEntryResponse"},"type":"array"},"entry_count":{"type":"integer"},"horizon":{"type":"string"},"id":{"type":"string"},"period_end":{"type":"string"},"period_start":{"type":"string"},"replaced_at":{"type":"string"},"text":{"description":"Text is the live entries' texts joined with \"\\n\". Read-only and\ndeprecated: read entries instead.","type":"string","x-deprecated":"true"},"updated_at":{"description":"UpdatedAt is when the goal last changed.","type":"string"},"user_id":{"type":"string"}},"type":"object"},"handlers.LogEntryResponse":{"properties":{"created_at":{"type":"string"},"id":{"type":"string"},"occurred_at":{"type":"string"},"text":{"type":"string"},"updated_at":{"type":"string"},"user_id":{"type":"string"}},"type":"object"},"handlers.PeriodNoteResponse":{"properties":{"created_at":{"type":"string"},"horizon":{"type":"string"},"id":{"type":"string"},"note":{"type":"string"},"period_end":{"type":"string"},"period_start":{"type":"string"},"updated_at":{"type":"string"},"user_id":{"type":"string"}},"type":"object"},"handlers.ProblemDetail":{"properties":{"detail":{"example":"Missing or invalid JWT claims","type":"string"},"error_code":{"allOf":[{"$ref":"#/definitions/apierr.ErrorCode"}],"example":1001},"errors":{"items":{"$ref":"#/definitions/apierr.ProblemDetail"},"type":"array"},"instance":{"type":"string"},"status":{"example":401,"type":"integer"},"title":{"example":"Unauthorized","type":"string"},"type":{"example":"about:blank","type":"string"}},"type":"object"},"handlers.TimelinePeriodResponse":{"properties":{"goal":{"$ref":"#/definitions/handlers.HorizonGoalResponse"},"period_end":{"type":"string"},"period_start":{"type":"string"}},"type":"object"},"handlers.TimerSettingResponse":{"properties":{"auto_start_breaks":{"type":"boolean"},"auto_start_focus":{"type":"boolean"},"focus_duration_ms":{"type":"integer"},"long_break_ms":{"type":"integer"},"sessions_per_cycle":{"type":"integer"},"short_break_ms":{"type":"integer"},"updated_at":{"type":"string"},"user_id":{"type":"string"}},"type":"object"},"handlers.WorkSessionResponse":{"properties":{"activity_id":{"type":"string"},"container_id":{"type":"string"},"created_at":{"type":"string"},"deleted_at":{"type":"string"},"duration_ms":{"type":"integer"},"ended_at":{"type":"string"},"focus_level":{"type":"integer"},"has_overlap":{"type":"boolean"},"id":{"type":"string"},"mode":{"type":"string"},"note":{"type":"string"},"paused_ms":{"type":"integer"},"rating":{"maximum":5,"minimum":1,"type":"integer"},"session_kind":{"type":"string"},"started_at":{"type":"string"},"task_id":{"type":"string"},"updated_at":{"type":"string"},"user_id":{"type":"string"},"version":{"description":"Version increases on every change; send it back quoted as If-Match on PUT.","type":"integer"}},"type":"object"},"handlers.allTimeStatsResponse":{"properties":{"focusedHours":{"items":{"type":"integer"},"type":"array"},"generatedAt":{"type":"string"},"longestStreakEndDate":{"type":"string"},"longestStreakLength":{"type":"integer"},"totalFocusSeconds":{"type":"integer"},"totalFocusSessions":{"type":"integer"}},"type":"object"},"handlers.carryGoalEntriesRequest":{"properties":{"entry_ids":{"items":{"type":"string"},"type":"array"},"horizon":{"type":"string"},"period_start":{"type":"string"}},"type":"object"},"handlers.createActivityRequest":{"properties":{"color":{"example":"#4CAF50","type":"string"},"icon":{"example":"briefcase","type":"string"},"id":{"example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"name":{"example":"Work","type":"string"}},"type":"object"},"handlers.createGoalEntryRequest":{"properties":{"carried_from":{"description":"CarriedFrom stamps a copy a client carried locally (offline-first)\nrather than through /entries/carry. Without it the source keeps being\noffered for carry on every other client.","type":"string"},"done":{"type":"boolean"},"id":{"description":"ID is accepted from the client for offline-first parity with sessions.","type":"string"},"position":{"description":"Position omitted means append.","type":"integer"},"text":{"type":"string"}},"type":"object"},"handlers.createSessionRequest":{"properties":{"activity_id":{"type":"string"},"duration_ms":{"type":"integer"},"ended_at":{"type":"string"},"focus_level":{"type":"integer"},"id":{"type":"string"},"mode":{"type":"string"},"note":{"type":"string"},"paused_ms":{"type":"integer"},"started_at":{"type":"string"},"task_id":{"type":"string"}},"type":"object"},"handlers.createTaskRequest":{"properties":{"activity_id":{"type":"string"},"archived_at":{"type":"string"},"description":{"example":"Write a comprehensive report on the project progress","type":"string"},"due_date":{"description":"With due_date_has_time false, stored as 12:00 UTC on the date it falls on in the user's timezone.","type":"string"},"due_date_has_time":{"type":"boolean"},"energy_level":{"type":"string"},"external_ref":{"description":"Permanent `\u003csource\u003e:\u003cid\u003e` link to an item in an external system, e.g. `jira:PROJ-123`. Must be a lowercase source (letter, then up to 31 of a-z, 0-9, -), a colon, and a 1-200 character id with no whitespace or control characters. Unique per user, soft-deleted tasks included (duplicate → 409, error_code 4010); set on create only, never changed or cleared — PUT ignores it. Importer contract (documented, not enforced): the source owns title, priority and description, adds its own tags but never removes the user's, and sets activity on create only; the user owns status, energy level, due date, reminder, goal link and sessions, which the importer never writes. When an item leaves the source's open set, archive the task (never delete); when it returns, unarchive only if status is not DONE. A task the user soft-deleted keeps its ref reserved: deleting means stop tracking, so the importer must not recreate it. Find existing imports with GET /tasks?external_ref_prefix=\u003csource\u003e:.","example":"jira:PROJ-123","type":"string"},"goal_entry_id":{"type":"string"},"id":{"type":"string"},"is_archived":{"type":"boolean"},"is_daily_goal":{"type":"boolean"},"position":{"example":0,"type":"integer"},"priority":{"example":"MEDIUM","type":"string"},"recurrence_rule":{"type":"string"},"recurrence_source_id":{"type":"string"},"reminder_option":{"type":"string"},"reminder_time":{"type":"string"},"status":{"example":"TODO","type":"string"},"tags":{"items":{"type":"string"},"type":"array"},"title":{"example":"Complete project report","type":"string"}},"type":"object"},"handlers.dailySnapshotsResponse":{"properties":{"page":{"type":"integer"},"page_size":{"type":"integer"},"snapshots":{"items":{"$ref":"#/definitions/handlers.DailySnapshotResponse"},"type":"array"},"total_count":{"type":"integer"}},"type":"object"},"handlers.decisionRequest":{"properties":{"answers":{"additionalProperties":{"type":"string"},"description":"Answers merges into the stored map: prompt key (^[a-z0-9_]{1,40}$, see\nGET /decisions/prompts) -\u003e text (≤ 4000 characters), ≤ 64 KB in total.\n\"\" clears a key; omitted keys are kept.","example":{"reasons":"Better pay and a team I trust"},"type":"object"},"decided_at":{"type":"string"},"id":{"type":"string"},"review_task_id":{"type":"string"},"reviewed_at":{"type":"string"},"same_call":{"description":"SameCall is yes, no or unsure; \"\" clears it.","example":"yes","type":"string"},"title":{"maxLength":200,"type":"string"}},"type":"object"},"handlers.logEntryRequest":{"properties":{"id":{"type":"string"},"occurred_at":{"type":"string"},"text":{"type":"string"}},"type":"object"},"handlers.paginatedActivitiesResponse":{"properties":{"data":{"items":{"$ref":"#/definitions/handlers.ActivityResponse"},"type":"array"},"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"handlers.paginatedTasksResponse":{"properties":{"data":{"items":{"$ref":"#/definitions/handlers.restTaskResponse"},"type":"array"},"limit":{"type":"integer"},"offset":{"type":"integer"},"total":{"type":"integer"}},"type":"object"},"handlers.restTaskResponse":{"properties":{"activity_id":{"type":"string"},"archived_at":{"type":"string"},"completed_at":{"description":"CompletedAt is null while the task is not done.","type":"string"},"completed_pomodoros":{"type":"integer"},"created_at":{"type":"string"},"deleted_at":{"type":"string"},"description":{"type":"string"},"due_date":{"type":"string"},"due_date_has_time":{"type":"boolean"},"energy_level":{"type":"string"},"external_ref":{"type":"string"},"goal_entry_id":{"type":"string"},"id":{"type":"string"},"is_archived":{"type":"boolean"},"is_daily_goal":{"type":"boolean"},"last_activity_at":{"type":"string"},"position":{"type":"integer"},"priority":{"type":"string"},"recurrence_rule":{"description":"RecurrenceRule is null for a one-off task.","type":"string"},"recurrence_source_id":{"type":"string"},"reminder_option":{"type":"string"},"reminder_time":{"type":"string"},"status":{"type":"string"},"tags":{"items":{"type":"string"},"type":"array"},"title":{"type":"string"},"total_time_spent_seconds":{"type":"integer"},"updated_at":{"type":"string"},"user_id":{"type":"string"},"version":{"description":"Version increases on every change.","type":"integer"}},"type":"object"},"handlers.setHorizonGoalRequest":{"properties":{"id":{"description":"ID is the client's local goal id (offline-first), used only when the\nperiod has no goal yet. Otherwise the existing goal comes back and the\nclient adopts its id.","type":"string"},"period_start":{"type":"string"}},"type":"object"},"handlers.setPeriodNoteRequest":{"properties":{"note":{"type":"string"}},"type":"object"},"handlers.statsSnapshotResponse":{"properties":{"focusedHours":{"items":{"type":"integer"},"type":"array"},"generatedAt":{"type":"string"},"sessionCount":{"type":"integer"},"snapshotEndDate":{"type":"string"},"streak":{"$ref":"#/definitions/handlers.streakResponse"},"totalDurationMs":{"type":"integer"},"yearGrid":{"additionalProperties":{"type":"integer"},"type":"object"}},"type":"object"},"handlers.streakResponse":{"properties":{"currentStreakEndDate":{"description":"End of the trailing run (user zone) — the day a client extends\ncurrentStreakLength from. lastStreakDate is the longest run's end.","type":"string"},"currentStreakLength":{"type":"integer"},"lastStreakDate":{"type":"string"},"longestStreakLength":{"type":"integer"}},"type":"object"},"handlers.taskCompletionResponse":{"properties":{"completed_at":{"type":"string"},"deleted_at":{"description":"DeletedAt marks an undone completion: drop the event locally, it\ncounts toward neither totals nor streaks. Omitted when live.","type":"string"},"id":{"type":"string"},"occurrence_day":{"description":"OccurrenceDay is the UTC day the completion counts against — the\ngranularity at which at-most-one-per-occurrence is enforced. Clients group\non it to rebuild recurring counts.","type":"string"},"task_id":{"type":"string"}},"type":"object"},"handlers.taskCompletionsResponse":{"properties":{"completions":{"items":{"$ref":"#/definitions/handlers.taskCompletionResponse"},"type":"array"},"count":{"type":"integer"}},"type":"object"},"handlers.timerSettingsRequest":{"properties":{"auto_start_breaks":{"example":false,"type":"boolean"},"auto_start_focus":{"example":false,"type":"boolean"},"focus_duration_ms":{"example":1500000,"type":"integer"},"long_break_ms":{"example":900000,"type":"integer"},"sessions_per_cycle":{"example":4,"type":"integer"},"short_break_ms":{"example":300000,"type":"integer"}},"type":"object"},"handlers.updateActivityRequest":{"properties":{"color":{"example":"#2196F3","type":"string"},"icon":{"example":"home","type":"string"},"name":{"example":"Personal","type":"string"}},"type":"object"},"handlers.updateGoalEntryRequest":{"properties":{"done":{"type":"boolean"},"position":{"type":"integer"},"text":{"type":"string"}},"type":"object"},"handlers.updateSessionRequest":{"properties":{"activity_id":{"type":"string"},"duration_ms":{"type":"integer"},"ended_at":{"type":"string"},"focus_level":{"type":"integer"},"mode":{"type":"string"},"note":{"type":"string"},"paused_ms":{"type":"integer"},"started_at":{"type":"string"},"task_id":{"type":"string"}},"type":"object"},"handlers.updateTaskRequest":{"properties":{"activity_id":{"type":"string"},"archived_at":{"type":"string"},"description":{"example":"Updated task description","type":"string"},"due_date":{"description":"With due_date_has_time false, stored as 12:00 UTC on the date it falls on in the user's timezone.","type":"string"},"due_date_has_time":{"type":"boolean"},"energy_level":{"type":"string"},"goal_entry_id":{"type":"string"},"is_archived":{"type":"boolean"},"is_daily_goal":{"type":"boolean"},"position":{"example":1,"type":"integer"},"priority":{"type":"string"},"recurrence_rule":{"type":"string"},"recurrence_source_id":{"type":"string"},"reminder_option":{"type":"string"},"reminder_time":{"type":"string"},"status":{"example":"IN_PROGRESS","type":"string"},"tags":{"items":{"type":"string"},"type":"array"},"title":{"example":"Updated task title","type":"string"}},"type":"object"},"handlers.updateTimerStateRequest":{"properties":{"elapsed_ms":{"example":123456,"type":"integer"},"focus_level":{"example":5,"type":"integer"},"is_break":{"example":false,"type":"boolean"},"mode":{"example":"FOCUS","type":"string"},"originating_device":{"example":"550e8400-e29b-41d4-a716-446655440000","type":"string"},"started_at":{"type":"string"},"status":{"example":"RUNNING","type":"string"},"task_id":{"type":"string"},"timer_algorithm":{"example":"ADAPTIVE","type":"string"},"timer_id":{"example":"550e8400-e29b-41d4-a716-446655440000","type":"string"}},"type":"object"}},"host":"api.kombine.dev","info":{"contact":{"email":"support@kombine.dev","name":"Kombine Team","url":"https://kombine.dev"},"description":"Backend API for the Kombine productivity app — task management, work sessions, and cross-device sync.","license":{"name":"MIT","url":"https://opensource.org/licenses/MIT"},"termsOfService":"https://kombine.dev/terms","title":"Kombine API","version":"1.0","x-generated":"2026-11-12T10:17:03Z"},"paths":{"/activities":{"get":{"consumes":["application/json"],"description":"Retrieve all activities for the authenticated user","parameters":[{"description":"Maximum number of activities to return (default 100, max 500)","in":"query","name":"limit","type":"integer"},{"description":"Number of activities to skip (default 0, max 100000)","in":"query","name":"offset","type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.paginatedActivitiesResponse"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List activities","tags":["activities"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"sessions:read"},"post":{"consumes":["application/json"],"description":"Create a new activity for the authenticated user","parameters":[{"description":"Activity to create","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.createActivityRequest"}}],"produces":["application/json"],"responses":{"201":{"description":"Created","headers":{"Location":{"description":"/v1/activities/{id}","type":"string"}},"schema":{"$ref":"#/definitions/handlers.ActivityResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Create activity","tags":["activities"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"sessions:write"}},"/activities/icons":{"get":{"description":"Returns the canonical list of activity icon slug values accepted by the API","produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"type":"string"},"type":"array"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List icon slugs","tags":["activities"],"x-error-codes":[],"x-pat-permission":"sessions:read"}},"/activities/{id}":{"delete":{"consumes":["application/json"],"description":"Soft delete an activity for the authenticated user","parameters":[{"description":"Activity ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"403":{"description":"Forbidden","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Delete activity","tags":["activities"],"x-error-codes":[1001,4002,4006,5003,9001],"x-pat-permission":"sessions:write"},"put":{"consumes":["application/json"],"description":"Full replace of a user-owned activity: name is required, and an omitted or empty color or icon resets to the default (not the stored value). Built-in presets cannot be modified (404).","parameters":[{"description":"Activity ID","in":"path","name":"id","required":true,"type":"string"},{"description":"Activity updates","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.updateActivityRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.ActivityResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Update activity","tags":["activities"],"x-error-codes":[1001,4002,5003,9001],"x-pat-permission":"sessions:write"}},"/daily-snapshots":{"get":{"consumes":["application/json"],"description":"Returns paginated daily snapshots for the authenticated user within a date range","parameters":[{"description":"Start date (YYYY-MM-DD, inclusive)","in":"query","name":"from","required":true,"type":"string"},{"description":"End date (YYYY-MM-DD, inclusive)","in":"query","name":"to","required":true,"type":"string"},{"description":"Page number (default: 1)","in":"query","name":"page","type":"integer"},{"description":"Page size (default: 90, max: 366)","in":"query","name":"page_size","type":"integer"},{"description":"Deprecated alias of page_size; page_size wins when both are sent","in":"query","name":"pageSize","type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.dailySnapshotsResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List daily snapshots by date range","tags":["daily-snapshots"],"x-error-codes":[1001,5001,5002,5003,9001],"x-pat-permission":"stats:read"}},"/decisions":{"get":{"description":"Returns the caller's live decisions, most recently decided first.","parameters":[{"description":"Maximum decisions to return (default 100, max 500)","in":"query","name":"limit","type":"integer"},{"description":"Decisions to skip","in":"query","name":"offset","type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.DecisionResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List decisions","tags":["decisions"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"decisions:read"},"post":{"consumes":["application/json"],"description":"Only title is required. A phase 2 answer marks the decision reviewed and completes its check-back task. review_task_id must name a task the caller owns (create the task first); anything else is stored as null. Accepts a client-generated UUID; omitting it has the server mint one.","parameters":[{"description":"Decision to create","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.decisionRequest"}}],"produces":["application/json"],"responses":{"201":{"description":"Created","headers":{"Location":{"description":"/v1/decisions/{id}","type":"string"}},"schema":{"$ref":"#/definitions/handlers.DecisionResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Create a decision","tags":["decisions"],"x-error-codes":[1001,4003,5001,5002,5003,9001],"x-pat-permission":"decisions:write"}},"/decisions/prompts":{"get":{"description":"The known keys of a decision's answers map, in display order, with their phase and English prompt text. answers is a free-form object: any key matching ^[a-z0-9_]{1,40}$ is stored and returned, but only these drive the review rules. same_call (yes / no / unsure) is a phase 2 answer kept as its own field.","produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.DecisionPromptResponse"},"type":"array"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List decision prompts","tags":["decisions"],"x-error-codes":[],"x-pat-permission":"decisions:read"}},"/decisions/{id}":{"delete":{"description":"Soft delete; other devices receive the tombstone on their next pull. Deleting an open decision also deletes its check-back task unless that task is done.","parameters":[{"description":"Decision ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Delete a decision","tags":["decisions"],"x-error-codes":[1001,4012,5003,9001],"x-pat-permission":"decisions:write"},"get":{"parameters":[{"description":"Decision ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.DecisionResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Get a decision","tags":["decisions"],"x-error-codes":[1001,4012,5003,9001],"x-pat-permission":"decisions:read"},"put":{"consumes":["application/json"],"description":"Partial: omitted fields are kept. answers merges into the stored map (\"\" clears a key), so a client never drops answers to prompts it does not know. The first phase 2 answer sets reviewed_at and completes the check-back task; a phase 1 change on a reviewed decision sets edited_after_review_at.","parameters":[{"description":"Decision ID","in":"path","name":"id","required":true,"type":"string"},{"description":"Fields to change","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.decisionRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.DecisionResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Update a decision","tags":["decisions"],"x-error-codes":[1001,4012,5001,5002,5003,9001],"x-pat-permission":"decisions:write"}},"/error-codes":{"get":{"description":"Returns all defined error codes with metadata and the exact codes each endpoint can return. Use this to build exhaustive client-side error handlers before shipping.","produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.ErrorCodesResponse"}}},"summary":"Error code registry","tags":["meta"],"x-error-codes":[]}},"/horizon-goals":{"get":{"description":"Returns the current (non-replaced) horizon goal for each horizon that has one set","produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.HorizonGoalResponse"},"type":"array"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List current horizon goals","tags":["horizon-goals"],"x-error-codes":[1001,9001],"x-pat-permission":"goals:read"}},"/horizon-goals/entries/carry":{"post":{"consumes":["application/json"],"description":"Copies a set of undone entries into a target period, stamping each copy as carried. One call, because the UI is one tap. Done entries are rejected — only an unfinished intention is a carry candidate — and the whole call fails rather than carrying part of the set.","parameters":[{"description":"Entries and target period","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.carryGoalEntriesRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.GoalEntryResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"422":{"description":"Unprocessable Entity","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Carry entries into a period","tags":["horizon-goals"],"x-error-codes":[1001,4005,5001,5002,5003,9001],"x-pat-permission":"goals:write"}},"/horizon-goals/entries/{entryID}":{"delete":{"description":"Tombstones one entry so the deletion reaches the user's other devices.","parameters":[{"description":"Entry ID","in":"path","name":"entryID","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Delete an entry","tags":["horizon-goals"],"x-error-codes":[1001,4005,5003,9001],"x-pat-permission":"goals:write"},"patch":{"consumes":["application/json"],"description":"Patches one entry's text, done state or position. Every field is optional — a tick sends only done. Ticking is allowed at any time, during the period or after: ticking is not reviewing.","parameters":[{"description":"Entry ID","in":"path","name":"entryID","required":true,"type":"string"},{"description":"Fields to change","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.updateGoalEntryRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.GoalEntryResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Edit an entry","tags":["horizon-goals"],"x-error-codes":[1001,4005,5002,5003,9001],"x-pat-permission":"goals:write"}},"/horizon-goals/{horizon}":{"delete":{"description":"Marks the current horizon goal for the horizon as replaced (cleared). No-op if none is set.","parameters":[{"description":"Goal horizon (WEEKLY, MONTHLY, QUARTERLY, ANNUAL)","in":"path","name":"horizon","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Clear horizon goal","tags":["horizon-goals"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"goals:write"},"post":{"consumes":["application/json"],"description":"Creates the goal row for a period if the user has not written into it yet, and returns it with its entries. Idempotent — a period that already exists is returned unchanged. Carries no text: a period's content is its entries. An optional client `id` names a new period's goal (offline-first); it is ignored when the period already exists, and an id already in use is a 400.","parameters":[{"description":"Goal horizon (WEEKLY, MONTHLY, QUARTERLY, ANNUAL)","in":"path","name":"horizon","required":true,"type":"string"},{"description":"Target period","in":"body","name":"request","schema":{"$ref":"#/definitions/handlers.setHorizonGoalRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.HorizonGoalResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Ensure a period exists","tags":["horizon-goals"],"x-error-codes":[1001,5001,5003,9001],"x-pat-permission":"goals:write"}},"/horizon-goals/{horizon}/history":{"get":{"description":"Returns past horizon goals for a horizon, newest first, capped at 100","parameters":[{"description":"Goal horizon (WEEKLY, MONTHLY, QUARTERLY, ANNUAL)","in":"path","name":"horizon","required":true,"type":"string"},{"description":"Max results (default 4, max 100; out of range falls back to 4)","in":"query","name":"limit","type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.HorizonGoalResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Horizon goal history","tags":["horizon-goals"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"goals:read"}},"/horizon-goals/{horizon}/timeline":{"get":{"description":"Returns contiguous periods for a horizon, including periods with no goal. Replaces /history.","parameters":[{"description":"Goal horizon (WEEKLY, MONTHLY, QUARTERLY, ANNUAL)","in":"path","name":"horizon","required":true,"type":"string"},{"description":"Range start (YYYY-MM-DD), default 12 periods back","in":"query","name":"from","type":"string"},{"description":"Range end (YYYY-MM-DD), default one period ahead","in":"query","name":"to","type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.TimelinePeriodResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Horizon goal timeline","tags":["horizon-goals"],"x-error-codes":[1001,5001,5003,9001],"x-pat-permission":"goals:read"}},"/horizon-goals/{id}/entries":{"get":{"description":"Returns one period's intentions in the user's own order. Never sorted by done state.","parameters":[{"description":"Goal ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.GoalEntryResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List a period's entries","tags":["horizon-goals"],"x-error-codes":[1001,4005,5003,9001],"x-pat-permission":"goals:read"},"post":{"consumes":["application/json"],"description":"Writes one intention into a period. Costs typing a line and nothing else — ticking, carrying and linking are all optional.","parameters":[{"description":"Goal ID","in":"path","name":"id","required":true,"type":"string"},{"description":"The line to add","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.createGoalEntryRequest"}}],"produces":["application/json"],"responses":{"201":{"description":"Created","headers":{"Location":{"description":"/v1/horizon-goals/entries/{id}","type":"string"}},"schema":{"$ref":"#/definitions/handlers.GoalEntryResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"422":{"description":"Unprocessable Entity","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Add an entry to a period","tags":["horizon-goals"],"x-error-codes":[1001,4003,4005,5002,5003,9001],"x-pat-permission":"goals:write"}},"/log-entries":{"get":{"description":"Returns the caller's entries whose occurred_at falls in the range, oldest first. Range params match GET /sessions so one window fetches both.","parameters":[{"description":"ISO8601 lower bound (inclusive) on occurred_at, default 24h ago","in":"query","name":"since","type":"string"},{"description":"ISO8601 upper bound (inclusive) on occurred_at, default now","in":"query","name":"to","type":"string"},{"description":"Maximum entries to return (default 100, max 500)","in":"query","name":"limit","type":"integer"},{"description":"Entries to skip, counted back from the newest in the range (default 0, max 100000); each page is still returned oldest first","in":"query","name":"offset","type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.LogEntryResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List log entries","tags":["log-entries"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"sessions:read"},"post":{"consumes":["application/json"],"description":"Accepts a client-generated UUID for offline-first parity with sessions; omitting it has the server mint one.","parameters":[{"description":"Entry to create","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.logEntryRequest"}}],"produces":["application/json"],"responses":{"201":{"description":"Created","headers":{"Location":{"description":"/v1/log-entries/{id}","type":"string"}},"schema":{"$ref":"#/definitions/handlers.LogEntryResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Create a log entry","tags":["log-entries"],"x-error-codes":[1001,4003,5001,5002,5003,9001],"x-pat-permission":"sessions:write"}},"/log-entries/{id}":{"delete":{"description":"Hard delete — no tombstone is kept, so the entry simply stops appearing in its range.","parameters":[{"description":"Log entry ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Delete a log entry","tags":["log-entries"],"x-error-codes":[1001,4009,5003,9001],"x-pat-permission":"sessions:write"},"put":{"consumes":["application/json"],"description":"Full replace: text and occurred_at are both required and both written. The timestamp is user-editable by design — it says when the thing happened, not when it was typed.","parameters":[{"description":"Log entry ID","in":"path","name":"id","required":true,"type":"string"},{"description":"New text and timestamp","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.logEntryRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.LogEntryResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Update a log entry","tags":["log-entries"],"x-error-codes":[1001,4009,5001,5002,5003,9001],"x-pat-permission":"sessions:write"}},"/period-notes":{"get":{"description":"Returns notes for a horizon across a date range, including periods with no goal","parameters":[{"description":"Goal horizon (WEEKLY, MONTHLY, QUARTERLY, ANNUAL)","in":"query","name":"horizon","required":true,"type":"string"},{"description":"Range start (YYYY-MM-DD), default 12 periods back","in":"query","name":"from","type":"string"},{"description":"Range end (YYYY-MM-DD), default current period","in":"query","name":"to","type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.PeriodNoteResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List period notes","tags":["period-notes"],"x-error-codes":[1001,5001,5003,9001],"x-pat-permission":"goals:read"}},"/period-notes/{horizon}/{period_start}":{"delete":{"description":"Removes the note for one period","parameters":[{"description":"Goal horizon (WEEKLY, MONTHLY, QUARTERLY, ANNUAL)","in":"path","name":"horizon","required":true,"type":"string"},{"description":"Period start (YYYY-MM-DD)","in":"path","name":"period_start","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Clear a period note","tags":["period-notes"],"x-error-codes":[1001,4005,5001,5003,9001],"x-pat-permission":"goals:write"},"put":{"consumes":["application/json"],"description":"Full replace (an upsert): creates or replaces the note for one period; note is required. Allowed while the period is still running — a note is not a retrospective judgement.","parameters":[{"description":"Goal horizon (WEEKLY, MONTHLY, QUARTERLY, ANNUAL)","in":"path","name":"horizon","required":true,"type":"string"},{"description":"Period start (YYYY-MM-DD)","in":"path","name":"period_start","required":true,"type":"string"},{"description":"Note text","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.setPeriodNoteRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.PeriodNoteResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Write a period note","tags":["period-notes"],"x-error-codes":[1001,5001,5002,5003,9001],"x-pat-permission":"goals:write"}},"/sessions":{"get":{"consumes":["application/json"],"description":"Retrieve work sessions. `since` alone filters by updated_at (incremental sync, includes tombstones). `since`+`to` filters by started_at (calendar date range). `to` without `since` is rejected. `offset` pages within whichever ordering applies (updated_at, or started_at for a range).","parameters":[{"description":"ISO8601 lower-bound (exclusive). Alone: updated_at \u003e since. With `to`: started_at \u003e since.","in":"query","name":"since","type":"string"},{"description":"ISO8601 upper-bound (inclusive) for started_at range; requires since","in":"query","name":"to","type":"string"},{"description":"Maximum number of sessions to return (default 100, max 500)","in":"query","name":"limit","type":"integer"},{"description":"Number of sessions to skip (default 0, max 100000)","in":"query","name":"offset","type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"items":{"$ref":"#/definitions/handlers.WorkSessionResponse"},"type":"array"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List work sessions","tags":["sessions"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"sessions:read"},"post":{"consumes":["application/json"],"description":"Create a new work session for the authenticated user","parameters":[{"description":"Session to create","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.createSessionRequest"}}],"produces":["application/json"],"responses":{"201":{"description":"Created","headers":{"ETag":{"description":"The session's version, quoted","type":"string"},"Location":{"description":"/v1/sessions/{id}","type":"string"}},"schema":{"$ref":"#/definitions/handlers.WorkSessionResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Create work session","tags":["sessions"],"x-error-codes":[1001,4001,4003,5001,5002,5003,9001],"x-pat-permission":"sessions:write"}},"/sessions/{id}":{"delete":{"consumes":["application/json"],"description":"Soft delete a work session for the authenticated user","parameters":[{"description":"Session ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Delete work session","tags":["sessions"],"x-error-codes":[1001,4007,5003,9001],"x-pat-permission":"sessions:write"},"put":{"consumes":["application/json"],"description":"Partially update a work session. An omitted field keeps its stored value; an explicit null clears task_id, activity_id, ended_at, duration_ms, focus_level or mode, and clears note to \"\". started_at and paused_ms cannot be cleared: null leaves them unchanged.","parameters":[{"description":"Session ID","in":"path","name":"id","required":true,"type":"string"},{"description":"Session updates","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.updateSessionRequest"}},{"description":"Optional. The quoted version last read (the ETag header, or the version field wrapped in double quotes); 412 when the session has moved on. Omit for an unconditional write","in":"header","name":"If-Match","type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","headers":{"ETag":{"description":"The session's version, quoted","type":"string"}},"schema":{"$ref":"#/definitions/handlers.WorkSessionResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"412":{"description":"Precondition Failed","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Update work session","tags":["sessions"],"x-error-codes":[1001,4001,4007,4015,5003,9001],"x-pat-permission":"sessions:write"}},"/stats/all-time":{"get":{"description":"Returns all-time statistics for the authenticated user, covering every","produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.allTimeStatsResponse"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Get all-time stats aggregate","tags":["stats"],"x-error-codes":[1001,4004,9001],"x-pat-permission":"stats:read"}},"/stats/snapshot":{"get":{"description":"Returns the pre-computed stats snapshot for a given year for the authenticated user.","parameters":[{"description":"Year (e.g. 2025)","in":"query","name":"year","required":true,"type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.statsSnapshotResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Get annual stats snapshot","tags":["stats"],"x-error-codes":[1001,4004,5002,5003,9001],"x-pat-permission":"stats:read"}},"/tasks":{"get":{"consumes":["application/json"],"description":"Retrieve the authenticated user's tasks. Archived tasks are excluded unless include_archived=true; with since set they are always included, so an incremental caller still learns a task was archived. status narrows to the listed statuses. total counts the filtered set. With external_ref_prefix, returns instead every task whose external_ref starts with the prefix — archived and soft-deleted ones included (deleted ones carry deleted_at) — ordered by external_ref, so an importer can diff against its source in one call; include_archived and status cannot be combined with it.","parameters":[{"description":"ISO8601 timestamp for incremental sync","in":"query","name":"since","type":"string"},{"description":"Match tasks whose external_ref starts with this, e.g. jira: (cannot be combined with since)","in":"query","name":"external_ref_prefix","type":"string"},{"description":"Include archived tasks (default false; ignored when since is set)","in":"query","name":"include_archived","type":"boolean"},{"description":"Comma-separated statuses to include: TODO, IN_PROGRESS, DONE (default all)","in":"query","name":"status","type":"string"},{"description":"Maximum number of tasks to return (default 200, max 500)","in":"query","name":"limit","type":"integer"},{"description":"Number of tasks to skip (default 0, max 100000)","in":"query","name":"offset","type":"integer"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.paginatedTasksResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List tasks","tags":["tasks"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"tasks:read"},"post":{"consumes":["application/json"],"description":"Create a new task for the authenticated user","parameters":[{"description":"Task to create","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.createTaskRequest"}}],"produces":["application/json"],"responses":{"201":{"description":"Created","headers":{"ETag":{"description":"The task's version, quoted","type":"string"},"Location":{"description":"/v1/tasks/{id}","type":"string"}},"schema":{"$ref":"#/definitions/handlers.restTaskResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Create task","tags":["tasks"],"x-error-codes":[1001,4003,4010,5001,5002,5003,9001],"x-pat-permission":"tasks:write"}},"/tasks/{id}":{"delete":{"consumes":["application/json"],"description":"Soft delete a task for the authenticated user","parameters":[{"description":"Task ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Delete task","tags":["tasks"],"x-error-codes":[1001,4001,5003,9001],"x-pat-permission":"tasks:write"},"put":{"consumes":["application/json"],"description":"Partial update: an omitted field keeps its stored value. An explicit null clears a nullable field (activity_id, goal_entry_id, due_date, reminder_time, archived_at, recurrence_rule, recurrence_source_id); for description, status, energy_level, priority and reminder_option an empty string clears too. title, position, is_archived and is_daily_goal cannot be cleared (null keeps them). A tags array replaces the whole list (null keeps it). external_ref is immutable: a sent value is ignored.","parameters":[{"description":"Task ID","in":"path","name":"id","required":true,"type":"string"},{"description":"Task updates","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.updateTaskRequest"}},{"description":"Optional. The quoted version last read (the ETag header, or the version field wrapped in double quotes); 412 when the task has moved on. Omit for an unconditional write","in":"header","name":"If-Match","type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","headers":{"ETag":{"description":"The task's version, quoted","type":"string"}},"schema":{"$ref":"#/definitions/handlers.restTaskResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"412":{"description":"Precondition Failed","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Update task","tags":["tasks"],"x-error-codes":[1001,4001,4015,5003,9001],"x-pat-permission":"tasks:write"}},"/tasks/{id}/completions":{"get":{"description":"Completion-event history for a recurring (or one-off) task. Count covers the optional from/to RFC3339 window (defaults to all-time); completions is the most-recent slice. The same task_id repeats across events by design — that is what makes \"completed N times\" computable.","parameters":[{"description":"Task ID","in":"path","name":"id","required":true,"type":"string"},{"description":"RFC3339 lower bound (inclusive) for count","in":"query","name":"from","type":"string"},{"description":"RFC3339 upper bound (exclusive) for count","in":"query","name":"to","type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.taskCompletionsResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"List task completions","tags":["tasks"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"tasks:read"}},"/tasks/{id}/skip":{"post":{"description":"Pass over the current due date. A recurring task advances as a completion would, to its first date after today, and resets to TODO. A one-time task moves to tomorrow in the user's time zone and keeps its status and time of day. No completion is recorded, so stats, streaks and goal progress are unchanged. A task that is due on a future date, has no due date, is done or is archived returns 400 with error code 4011 and writes nothing. Skipping a due date that is already skipped returns the task without moving it again.","parameters":[{"description":"Task ID","in":"path","name":"id","required":true,"type":"string"}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.restTaskResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Skip a task due today or overdue","tags":["tasks"],"x-error-codes":[1001,4001,4011,5003,9001],"x-pat-permission":"tasks:write"}},"/timer/settings":{"get":{"consumes":["application/json"],"description":"Retrieve timer settings for the authenticated user (returns defaults if not set)","produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.TimerSettingResponse"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Get timer settings","tags":["settings"],"x-error-codes":[1001,9001],"x-pat-permission":"timer:read"},"put":{"consumes":["application/json"],"description":"Full replace: every field is written. focus_duration_ms, short_break_ms, long_break_ms and sessions_per_cycle are required (an omitted one fails validation); an omitted auto_start_breaks or auto_start_focus is stored as false, not kept. Send the whole object.","parameters":[{"description":"Timer settings","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.timerSettingsRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.TimerSettingResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Update timer settings","tags":["settings"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"timer:write"}},"/timer/state":{"delete":{"consumes":["application/json"],"description":"Clear active timer state for the authenticated user. device_id names the device the clear is announced from; it defaults to the access token's device. A personal access token has no device, so a PAT caller must still send it.","parameters":[{"description":"Device ID (defaults to the access token's device)","in":"query","name":"device_id","type":"string"}],"produces":["application/json"],"responses":{"204":{"description":"No Content"},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Clear timer state","tags":["timer-state"],"x-error-codes":[1001,5002,9001],"x-pat-permission":"timer:write"},"get":{"consumes":["application/json"],"description":"Retrieve active timer state for the authenticated user","produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.ActiveTimerStateResponse"}},"204":{"description":"No Content"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Get timer state","tags":["timer-state"],"x-error-codes":[1001,9001],"x-pat-permission":"timer:read"},"put":{"consumes":["application/json"],"description":"Full replace of the user's single active timer state (an upsert): every field is written, so an omitted optional field (task_id, focus_level, originating_device, timer_algorithm, started_at) is stored as null and is_break as false. Send the complete snapshot each time.","parameters":[{"description":"Timer state update","in":"body","name":"request","required":true,"schema":{"$ref":"#/definitions/handlers.updateTimerStateRequest"}}],"produces":["application/json"],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.ActiveTimerStateResponse"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}},"500":{"description":"Internal Server Error","schema":{"$ref":"#/definitions/handlers.ProblemDetail"}}},"security":[{"PersonalAccessToken":[]}],"summary":"Update timer state","tags":["timer-state"],"x-error-codes":[1001,5003,9001],"x-pat-permission":"timer:write"}}},"securityDefinitions":{"PersonalAccessToken":{"description":"A personal access token created in the webapp, sent as \"Bearer kmb_pat_…\". Only operations listing this scheme accept it, and only with the permission named in their x-pat-permission (GET/HEAD need read, other methods write).","in":"header","name":"Authorization","type":"apiKey"}},"swagger":"2.0"}