openapi: 3.0.3 info: title: Casera (MyCrib) API description: | REST API for the Casera property management platform. Consumed by iOS (SwiftUI) and Android (Compose) mobile clients via Kotlin Multiplatform. ## Authentication Token-based authentication. After login/register, include the token in subsequent requests: ``` Authorization: Token ``` `Bearer ` is also accepted. ## Common Headers | Header | Description | Required | |--------|-------------|----------| | `Authorization` | `Token ` or `Bearer ` | Protected routes | | `X-Timezone` | IANA timezone (e.g. `America/Chicago`) for overdue calculations | Optional | | `Accept-Language` | Locale for i18n error messages (e.g. `en`, `es`) | Optional | | `If-None-Match` | ETag for conditional requests on `/api/static_data/` | Optional | ## Error Responses All errors follow a consistent JSON shape. Validation errors include a `details` map. version: 2.0.0 contact: name: Casera Team servers: - url: https://mycrib.treytartt.com/api description: Production - url: http://127.0.0.1:8000/api description: Local development (iOS simulator) - url: http://10.0.2.2:8000/api description: Local development (Android emulator) tags: - name: Auth (Public) description: Authentication endpoints that do not require a token - name: Auth (Protected) description: Authentication endpoints that require a token - name: Static Data description: Lookup/reference data (public, cached with ETag) - name: Residences description: Property management (protected) - name: Tasks description: Task management and kanban board (protected) - name: Task Completions description: Task completion records (protected) - name: Contractors description: Contractor management (protected) - name: Documents description: Document and warranty management (protected) - name: Notifications description: Push notifications and preferences (protected) - name: Subscriptions description: In-app subscription management - name: Uploads description: File upload endpoints (protected) - name: Media description: Authenticated media serving (protected) paths: # =========================================================================== # Auth (Public) # =========================================================================== /auth/login/: post: tags: [Auth (Public)] operationId: login summary: Login with email/password requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' responses: '200': description: Successful login content: application/json: schema: $ref: '#/components/schemas/LoginResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Error' /auth/register/: post: tags: [Auth (Public)] operationId: register summary: Register a new user requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegisterRequest' responses: '201': description: Registration successful content: application/json: schema: $ref: '#/components/schemas/RegisterResponse' '400': $ref: '#/components/responses/ValidationError' '409': $ref: '#/components/responses/Error' /auth/forgot-password/: post: tags: [Auth (Public)] operationId: forgotPassword summary: Send password reset code to email requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ForgotPasswordRequest' responses: '200': description: Reset code sent content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': $ref: '#/components/responses/ValidationError' /auth/verify-reset-code/: post: tags: [Auth (Public)] operationId: verifyResetCode summary: Verify password reset code requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VerifyResetCodeRequest' responses: '200': description: Code verified, returns reset token content: application/json: schema: $ref: '#/components/schemas/VerifyResetCodeResponse' '400': $ref: '#/components/responses/Error' /auth/reset-password/: post: tags: [Auth (Public)] operationId: resetPassword summary: Reset password using reset token requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ResetPasswordRequest' responses: '200': description: Password reset successful content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': $ref: '#/components/responses/Error' /auth/apple-sign-in/: post: tags: [Auth (Public)] operationId: appleSignIn summary: Sign in with Apple ID token requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AppleSignInRequest' responses: '200': description: Successful Apple sign-in content: application/json: schema: $ref: '#/components/schemas/SocialSignInResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' /auth/google-sign-in/: post: tags: [Auth (Public)] operationId: googleSignIn summary: Sign in with Google ID token requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/GoogleSignInRequest' responses: '200': description: Successful Google sign-in content: application/json: schema: $ref: '#/components/schemas/SocialSignInResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Error' # =========================================================================== # Auth (Protected) # =========================================================================== /auth/logout/: post: tags: [Auth (Protected)] operationId: logout summary: Logout (invalidate token) security: - tokenAuth: [] responses: '200': description: Logged out content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' /auth/me/: get: tags: [Auth (Protected)] operationId: getCurrentUser summary: Get current authenticated user with profile security: - tokenAuth: [] responses: '200': description: Current user content: application/json: schema: $ref: '#/components/schemas/CurrentUserResponse' '401': $ref: '#/components/responses/Unauthorized' /auth/profile/: put: tags: [Auth (Protected)] operationId: updateProfile summary: Update user profile security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateProfileRequest' responses: '200': description: Profile updated content: application/json: schema: $ref: '#/components/schemas/CurrentUserResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' patch: tags: [Auth (Protected)] operationId: patchProfile summary: Partial update user profile security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateProfileRequest' responses: '200': description: Profile updated content: application/json: schema: $ref: '#/components/schemas/CurrentUserResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' /auth/verify-email/: post: tags: [Auth (Protected)] operationId: verifyEmail summary: Verify email with 6-digit code security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/VerifyEmailRequest' responses: '200': description: Email verified content: application/json: schema: $ref: '#/components/schemas/VerifyEmailResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /auth/resend-verification/: post: tags: [Auth (Protected)] operationId: resendVerification summary: Resend email verification code security: - tokenAuth: [] responses: '200': description: Verification email sent content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' # =========================================================================== # Static Data (Public) # =========================================================================== /static_data/: get: tags: [Static Data] operationId: getStaticData summary: Get all lookup/reference data (ETag support) description: | Returns all seeded lookup data in a single payload. Supports conditional requests via `If-None-Match` header -- returns 304 when data has not changed. parameters: - in: header name: If-None-Match schema: type: string description: ETag from a previous response responses: '200': description: All lookup data headers: ETag: schema: type: string description: Content hash for conditional requests Cache-Control: schema: type: string description: "private, max-age=3600" content: application/json: schema: $ref: '#/components/schemas/SeededDataResponse' '304': description: Not Modified (client ETag matches) /residences/types/: get: tags: [Static Data] operationId: getResidenceTypes summary: List residence types responses: '200': description: Residence types content: application/json: schema: type: array items: $ref: '#/components/schemas/ResidenceTypeResponse' /tasks/categories/: get: tags: [Static Data] operationId: getTaskCategories summary: List task categories responses: '200': description: Task categories content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskCategoryResponse' /tasks/priorities/: get: tags: [Static Data] operationId: getTaskPriorities summary: List task priorities responses: '200': description: Task priorities content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskPriorityResponse' /tasks/frequencies/: get: tags: [Static Data] operationId: getTaskFrequencies summary: List task frequencies responses: '200': description: Task frequencies content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskFrequencyResponse' /contractors/specialties/: get: tags: [Static Data] operationId: getContractorSpecialties summary: List contractor specialties responses: '200': description: Contractor specialties content: application/json: schema: type: array items: $ref: '#/components/schemas/ContractorSpecialtyResponse' /tasks/templates/: get: tags: [Static Data] operationId: getTaskTemplates summary: List all task templates responses: '200': description: Task templates content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskTemplateResponse' /tasks/templates/grouped/: get: tags: [Static Data] operationId: getTaskTemplatesGrouped summary: Get task templates grouped by category responses: '200': description: Templates grouped by category content: application/json: schema: $ref: '#/components/schemas/TaskTemplatesGroupedResponse' /tasks/templates/search/: get: tags: [Static Data] operationId: searchTaskTemplates summary: Search task templates by query parameters: - in: query name: q schema: type: string description: Search query string responses: '200': description: Matching templates content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskTemplateResponse' /tasks/templates/by-category/{category_id}/: get: tags: [Static Data] operationId: getTaskTemplatesByCategory summary: Get templates for a specific category parameters: - $ref: '#/components/parameters/CategoryIdParam' responses: '200': description: Templates for category content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskTemplateResponse' /tasks/templates/{id}/: get: tags: [Static Data] operationId: getTaskTemplate summary: Get a single task template parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task template content: application/json: schema: $ref: '#/components/schemas/TaskTemplateResponse' '404': $ref: '#/components/responses/NotFound' # =========================================================================== # Residences # =========================================================================== /residences/: get: tags: [Residences] operationId: listResidences summary: List all residences the user has access to security: - tokenAuth: [] responses: '200': description: List of residences content: application/json: schema: type: array items: $ref: '#/components/schemas/ResidenceResponse' '401': $ref: '#/components/responses/Unauthorized' post: tags: [Residences] operationId: createResidence summary: Create a new residence security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateResidenceRequest' responses: '201': description: Residence created content: application/json: schema: $ref: '#/components/schemas/ResidenceWithSummaryResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /residences/my-residences/: get: tags: [Residences] operationId: getMyResidences summary: Get user residences (optimized for main screen) security: - tokenAuth: [] responses: '200': description: Residences with metadata content: application/json: schema: $ref: '#/components/schemas/MyResidencesResponse' '401': $ref: '#/components/responses/Unauthorized' /residences/summary/: get: tags: [Residences] operationId: getResidenceSummary summary: Get summary counts across all residences security: - tokenAuth: [] responses: '200': description: Summary statistics content: application/json: schema: $ref: '#/components/schemas/TotalSummary' '401': $ref: '#/components/responses/Unauthorized' /residences/join-with-code/: post: tags: [Residences] operationId: joinWithCode summary: Join a residence using a share code security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/JoinWithCodeRequest' responses: '200': description: Joined residence content: application/json: schema: $ref: '#/components/schemas/JoinResidenceResponse' '400': $ref: '#/components/responses/Error' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Error' /residences/{id}/: get: tags: [Residences] operationId: getResidence summary: Get a single residence security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Residence detail content: application/json: schema: $ref: '#/components/schemas/ResidenceResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' put: tags: [Residences] operationId: updateResidence summary: Update a residence security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateResidenceRequest' responses: '200': description: Residence updated content: application/json: schema: $ref: '#/components/schemas/ResidenceWithSummaryResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: tags: [Residences] operationId: patchResidence summary: Partial update a residence security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateResidenceRequest' responses: '200': description: Residence updated content: application/json: schema: $ref: '#/components/schemas/ResidenceWithSummaryResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' delete: tags: [Residences] operationId: deleteResidence summary: Delete a residence (owner only) security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Residence deleted content: application/json: schema: $ref: '#/components/schemas/ResidenceDeleteWithSummaryResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /residences/{id}/generate-share-code/: post: tags: [Residences] operationId: generateShareCode summary: Generate a share code for a residence security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: content: application/json: schema: $ref: '#/components/schemas/GenerateShareCodeRequest' responses: '201': description: Share code generated content: application/json: schema: $ref: '#/components/schemas/GenerateShareCodeResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /residences/{id}/generate-share-package/: post: tags: [Residences] operationId: generateSharePackage summary: Generate a share package (.casera file metadata) security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Share package data content: application/json: schema: $ref: '#/components/schemas/SharePackageResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /residences/{id}/share-code/: get: tags: [Residences] operationId: getShareCode summary: Get active share code for a residence security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Active share code content: application/json: schema: $ref: '#/components/schemas/ShareCodeResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /residences/{id}/users/: get: tags: [Residences] operationId: getResidenceUsers summary: List users who have access to a residence security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: List of residence users content: application/json: schema: type: array items: $ref: '#/components/schemas/ResidenceUserResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /residences/{id}/users/{user_id}/: delete: tags: [Residences] operationId: removeResidenceUser summary: Remove a user from a residence (owner only) security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' - in: path name: user_id required: true schema: type: integer format: uint description: ID of the user to remove responses: '200': description: User removed content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /residences/{id}/generate-tasks-report/: post: tags: [Residences] operationId: generateTasksReport summary: Generate a PDF tasks report for a residence security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: PDF report content: application/pdf: schema: type: string format: binary '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # =========================================================================== # Tasks # =========================================================================== /tasks/: get: tags: [Tasks] operationId: listTasks summary: Get kanban board with all user tasks description: Returns tasks organized into kanban columns (overdue, due_soon, upcoming, completed, in_progress, archived). security: - tokenAuth: [] responses: '200': description: Kanban board content: application/json: schema: $ref: '#/components/schemas/KanbanBoardResponse' '401': $ref: '#/components/responses/Unauthorized' post: tags: [Tasks] operationId: createTask summary: Create a new task security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateTaskRequest' responses: '201': description: Task created content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /tasks/by-residence/{residence_id}/: get: tags: [Tasks] operationId: getTasksByResidence summary: Get kanban board for a specific residence security: - tokenAuth: [] parameters: - in: path name: residence_id required: true schema: type: integer format: uint description: Residence ID responses: '200': description: Kanban board for residence content: application/json: schema: $ref: '#/components/schemas/KanbanBoardResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /tasks/{id}/: get: tags: [Tasks] operationId: getTask summary: Get task detail security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task detail content: application/json: schema: $ref: '#/components/schemas/TaskResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' put: tags: [Tasks] operationId: updateTask summary: Update a task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateTaskRequest' responses: '200': description: Task updated content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: tags: [Tasks] operationId: patchTask summary: Partial update a task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateTaskRequest' responses: '200': description: Task updated content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' delete: tags: [Tasks] operationId: deleteTask summary: Delete a task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task deleted content: application/json: schema: $ref: '#/components/schemas/DeleteWithSummaryResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /tasks/{id}/mark-in-progress/: post: tags: [Tasks] operationId: markTaskInProgress summary: Mark a task as in progress security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task marked in progress content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /tasks/{id}/cancel/: post: tags: [Tasks] operationId: cancelTask summary: Cancel a task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task cancelled content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /tasks/{id}/uncancel/: post: tags: [Tasks] operationId: uncancelTask summary: Uncancel a previously cancelled task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task uncancelled content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /tasks/{id}/archive/: post: tags: [Tasks] operationId: archiveTask summary: Archive a task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task archived content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /tasks/{id}/unarchive/: post: tags: [Tasks] operationId: unarchiveTask summary: Unarchive a previously archived task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task unarchived content: application/json: schema: $ref: '#/components/schemas/TaskWithSummaryResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /tasks/{id}/quick-complete/: post: tags: [Tasks] operationId: quickCompleteTask summary: Quick complete a task (from widget, no notes/images) security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task completed content: application/json: schema: $ref: '#/components/schemas/TaskCompletionWithSummaryResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /tasks/{id}/completions/: get: tags: [Tasks] operationId: getTaskCompletions summary: List completions for a specific task security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Task completions content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskCompletionResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' # =========================================================================== # Task Completions # =========================================================================== /task-completions/: get: tags: [Task Completions] operationId: listCompletions summary: List all task completions for the user security: - tokenAuth: [] responses: '200': description: List of completions content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskCompletionResponse' '401': $ref: '#/components/responses/Unauthorized' post: tags: [Task Completions] operationId: createCompletion summary: Create a task completion (with optional images) security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateTaskCompletionRequest' responses: '201': description: Completion created (includes updated task) content: application/json: schema: $ref: '#/components/schemas/TaskCompletionWithSummaryResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /task-completions/{id}/: get: tags: [Task Completions] operationId: getCompletion summary: Get a single task completion security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Completion detail content: application/json: schema: $ref: '#/components/schemas/TaskCompletionResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' put: tags: [Task Completions] operationId: updateCompletion summary: Update a task completion security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateTaskCompletionRequest' responses: '200': description: Completion updated content: application/json: schema: $ref: '#/components/schemas/TaskCompletionResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' delete: tags: [Task Completions] operationId: deleteCompletion summary: Delete a task completion security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Completion deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' # =========================================================================== # Contractors # =========================================================================== /contractors/: get: tags: [Contractors] operationId: listContractors summary: List all contractors accessible to the user security: - tokenAuth: [] responses: '200': description: List of contractors content: application/json: schema: type: array items: $ref: '#/components/schemas/ContractorResponse' '401': $ref: '#/components/responses/Unauthorized' post: tags: [Contractors] operationId: createContractor summary: Create a new contractor security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateContractorRequest' responses: '201': description: Contractor created content: application/json: schema: $ref: '#/components/schemas/ContractorResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /contractors/by-residence/{residence_id}/: get: tags: [Contractors] operationId: listContractorsByResidence summary: List contractors for a specific residence security: - tokenAuth: [] parameters: - in: path name: residence_id required: true schema: type: integer format: uint description: Residence ID responses: '200': description: Contractors for residence content: application/json: schema: type: array items: $ref: '#/components/schemas/ContractorResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /contractors/{id}/: get: tags: [Contractors] operationId: getContractor summary: Get a single contractor security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Contractor detail content: application/json: schema: $ref: '#/components/schemas/ContractorResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' put: tags: [Contractors] operationId: updateContractor summary: Update a contractor security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateContractorRequest' responses: '200': description: Contractor updated content: application/json: schema: $ref: '#/components/schemas/ContractorResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: tags: [Contractors] operationId: patchContractor summary: Partial update a contractor security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateContractorRequest' responses: '200': description: Contractor updated content: application/json: schema: $ref: '#/components/schemas/ContractorResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' delete: tags: [Contractors] operationId: deleteContractor summary: Delete a contractor security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Contractor deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /contractors/{id}/toggle-favorite/: post: tags: [Contractors] operationId: toggleContractorFavorite summary: Toggle favorite status of a contractor security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Favorite toggled content: application/json: schema: $ref: '#/components/schemas/ToggleFavoriteResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /contractors/{id}/tasks/: get: tags: [Contractors] operationId: getContractorTasks summary: Get tasks associated with a contractor security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Contractor tasks content: application/json: schema: type: array items: $ref: '#/components/schemas/TaskResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' # =========================================================================== # Documents # =========================================================================== /documents/: get: tags: [Documents] operationId: listDocuments summary: List all documents accessible to the user security: - tokenAuth: [] responses: '200': description: List of documents content: application/json: schema: type: array items: $ref: '#/components/schemas/DocumentResponse' '401': $ref: '#/components/responses/Unauthorized' post: tags: [Documents] operationId: createDocument summary: Create a new document security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateDocumentRequest' responses: '201': description: Document created content: application/json: schema: $ref: '#/components/schemas/DocumentResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /documents/warranties/: get: tags: [Documents] operationId: listWarranties summary: List warranty documents security: - tokenAuth: [] responses: '200': description: Warranty documents content: application/json: schema: type: array items: $ref: '#/components/schemas/DocumentResponse' '401': $ref: '#/components/responses/Unauthorized' /documents/{id}/: get: tags: [Documents] operationId: getDocument summary: Get a single document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Document detail content: application/json: schema: $ref: '#/components/schemas/DocumentResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' put: tags: [Documents] operationId: updateDocument summary: Update a document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateDocumentRequest' responses: '200': description: Document updated content: application/json: schema: $ref: '#/components/schemas/DocumentResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: tags: [Documents] operationId: patchDocument summary: Partial update a document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateDocumentRequest' responses: '200': description: Document updated content: application/json: schema: $ref: '#/components/schemas/DocumentResponse' '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' delete: tags: [Documents] operationId: deleteDocument summary: Delete a document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Document deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /documents/{id}/activate/: post: tags: [Documents] operationId: activateDocument summary: Activate a document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Document activated content: application/json: schema: type: object properties: message: type: string document: $ref: '#/components/schemas/DocumentResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /documents/{id}/deactivate/: post: tags: [Documents] operationId: deactivateDocument summary: Deactivate a document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Document deactivated content: application/json: schema: type: object properties: message: type: string document: $ref: '#/components/schemas/DocumentResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /documents/{id}/images/: post: tags: [Documents] operationId: uploadDocumentImage summary: Upload an image to a document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: multipart/form-data: schema: type: object required: - image properties: image: type: string format: binary description: Image file to upload caption: type: string description: Optional caption for the image responses: '201': description: Image uploaded successfully content: application/json: schema: $ref: '#/components/schemas/DocumentResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /documents/{id}/images/{imageId}/: delete: tags: [Documents] operationId: deleteDocumentImage summary: Delete an image from a document security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' - in: path name: imageId required: true schema: type: integer description: Image ID responses: '200': description: Image deleted, returns updated document content: application/json: schema: $ref: '#/components/schemas/DocumentResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # =========================================================================== # Notifications # =========================================================================== /notifications/: get: tags: [Notifications] operationId: listNotifications summary: List user notifications security: - tokenAuth: [] parameters: - in: query name: limit schema: type: integer default: 50 description: Max number of notifications to return - in: query name: offset schema: type: integer default: 0 description: Offset for pagination responses: '200': description: Notifications list content: application/json: schema: $ref: '#/components/schemas/NotificationListResponse' '401': $ref: '#/components/responses/Unauthorized' /notifications/unread-count/: get: tags: [Notifications] operationId: getUnreadCount summary: Get count of unread notifications security: - tokenAuth: [] responses: '200': description: Unread count content: application/json: schema: type: object required: [unread_count] properties: unread_count: type: integer '401': $ref: '#/components/responses/Unauthorized' /notifications/{id}/read/: post: tags: [Notifications] operationId: markNotificationRead summary: Mark a single notification as read security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Marked as read content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' /notifications/mark-all-read/: post: tags: [Notifications] operationId: markAllNotificationsRead summary: Mark all notifications as read security: - tokenAuth: [] responses: '200': description: All marked as read content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' /notifications/devices/: post: tags: [Notifications] operationId: registerDevice summary: Register a push notification device security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegisterDeviceRequest' responses: '201': description: Device registered content: application/json: schema: $ref: '#/components/schemas/DeviceResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' get: tags: [Notifications] operationId: listDevices summary: List registered push devices security: - tokenAuth: [] responses: '200': description: List of devices content: application/json: schema: type: array items: $ref: '#/components/schemas/DeviceResponse' '401': $ref: '#/components/responses/Unauthorized' /notifications/devices/register/: post: tags: [Notifications] operationId: registerDeviceAlias summary: Register a push notification device (alias) description: Alias for POST /notifications/devices/ for mobile client compatibility. security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegisterDeviceRequest' responses: '201': description: Device registered content: application/json: schema: $ref: '#/components/schemas/DeviceResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /notifications/devices/unregister/: post: tags: [Notifications] operationId: unregisterDevice summary: Unregister a push device by registration ID security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UnregisterDeviceRequest' responses: '200': description: Device unregistered content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /notifications/devices/{id}/: delete: tags: [Notifications] operationId: deleteDevice summary: Delete a registered push device security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' - in: query name: platform schema: type: string enum: [ios, android] default: ios description: Device platform responses: '200': description: Device removed content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '401': $ref: '#/components/responses/Unauthorized' /notifications/preferences/: get: tags: [Notifications] operationId: getNotificationPreferences summary: Get notification preferences security: - tokenAuth: [] responses: '200': description: Notification preferences content: application/json: schema: $ref: '#/components/schemas/NotificationPreference' '401': $ref: '#/components/responses/Unauthorized' put: tags: [Notifications] operationId: updateNotificationPreferences summary: Update notification preferences security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdatePreferencesRequest' responses: '200': description: Preferences updated content: application/json: schema: $ref: '#/components/schemas/NotificationPreference' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' patch: tags: [Notifications] operationId: patchNotificationPreferences summary: Partial update notification preferences security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdatePreferencesRequest' responses: '200': description: Preferences updated content: application/json: schema: $ref: '#/components/schemas/NotificationPreference' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' # =========================================================================== # Subscriptions # =========================================================================== /subscription/: get: tags: [Subscriptions] operationId: getSubscription summary: Get current subscription security: - tokenAuth: [] responses: '200': description: Subscription details content: application/json: schema: $ref: '#/components/schemas/SubscriptionResponse' '401': $ref: '#/components/responses/Unauthorized' /subscription/status/: get: tags: [Subscriptions] operationId: getSubscriptionStatus summary: Get subscription status with limits and usage security: - tokenAuth: [] responses: '200': description: Subscription status content: application/json: schema: $ref: '#/components/schemas/SubscriptionStatusResponse' '401': $ref: '#/components/responses/Unauthorized' /subscription/features/: get: tags: [Subscriptions] operationId: getFeatureBenefits summary: List feature benefits (free vs pro comparison) security: - tokenAuth: [] responses: '200': description: Feature benefits list content: application/json: schema: type: array items: $ref: '#/components/schemas/FeatureBenefit' '401': $ref: '#/components/responses/Unauthorized' /subscription/promotions/: get: tags: [Subscriptions] operationId: getPromotions summary: List active promotions for the user security: - tokenAuth: [] responses: '200': description: Active promotions content: application/json: schema: type: array items: $ref: '#/components/schemas/Promotion' '401': $ref: '#/components/responses/Unauthorized' /subscription/upgrade-triggers/: get: tags: [Subscriptions] operationId: getAllUpgradeTriggers summary: Get all upgrade triggers (public, no auth required) description: Available without authentication so the app can display upgrade prompts before login. responses: '200': description: All upgrade triggers content: application/json: schema: type: array items: $ref: '#/components/schemas/UpgradeTriggerResponse' /subscription/upgrade-trigger/{key}/: get: tags: [Subscriptions] operationId: getUpgradeTrigger summary: Get a single upgrade trigger by key security: - tokenAuth: [] parameters: - in: path name: key required: true schema: type: string description: Trigger key (e.g. "properties_limit", "tasks_limit") responses: '200': description: Upgrade trigger content: application/json: schema: $ref: '#/components/schemas/UpgradeTriggerResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /subscription/purchase/: post: tags: [Subscriptions] operationId: processPurchase summary: Process an in-app purchase (iOS or Android) security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProcessPurchaseRequest' responses: '200': description: Purchase processed content: application/json: schema: type: object properties: message: type: string subscription: $ref: '#/components/schemas/SubscriptionResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /subscription/cancel/: post: tags: [Subscriptions] operationId: cancelSubscription summary: Cancel subscription security: - tokenAuth: [] responses: '200': description: Subscription cancelled content: application/json: schema: type: object properties: message: type: string subscription: $ref: '#/components/schemas/SubscriptionResponse' '401': $ref: '#/components/responses/Unauthorized' /subscription/restore/: post: tags: [Subscriptions] operationId: restoreSubscription summary: Restore a previous subscription security: - tokenAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProcessPurchaseRequest' responses: '200': description: Subscription restored content: application/json: schema: type: object properties: message: type: string subscription: $ref: '#/components/schemas/SubscriptionResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' # =========================================================================== # Uploads # =========================================================================== /uploads/image/: post: tags: [Uploads] operationId: uploadImage summary: Upload an image file security: - tokenAuth: [] parameters: - in: query name: category schema: type: string default: images description: Storage category/folder requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: '200': description: Upload result content: application/json: schema: $ref: '#/components/schemas/UploadResult' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /uploads/document/: post: tags: [Uploads] operationId: uploadDocument summary: Upload a document file security: - tokenAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: '200': description: Upload result content: application/json: schema: $ref: '#/components/schemas/UploadResult' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /uploads/completion/: post: tags: [Uploads] operationId: uploadCompletion summary: Upload a task completion image security: - tokenAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: '200': description: Upload result content: application/json: schema: $ref: '#/components/schemas/UploadResult' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' /uploads/: delete: tags: [Uploads] operationId: deleteUploadedFile summary: Delete an uploaded file security: - tokenAuth: [] requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: type: string description: URL of the file to delete responses: '200': description: File deleted content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '400': $ref: '#/components/responses/Error' '401': $ref: '#/components/responses/Unauthorized' # =========================================================================== # Media (Authenticated serving) # =========================================================================== /media/document/{id}: get: tags: [Media] operationId: serveDocument summary: Serve a document file (authenticated) description: Proxies the document file from S3 after verifying the user has access to the residence. security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: File content content: application/octet-stream: schema: type: string format: binary '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /media/document-image/{id}: get: tags: [Media] operationId: serveDocumentImage summary: Serve a document image (authenticated) security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Image content content: image/*: schema: type: string format: binary '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /media/completion-image/{id}: get: tags: [Media] operationId: serveCompletionImage summary: Serve a task completion image (authenticated) security: - tokenAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Image content content: image/*: schema: type: string format: binary '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # ============================================================================= # Components # ============================================================================= components: securitySchemes: tokenAuth: type: apiKey in: header name: Authorization description: 'Token-based auth. Format: `Token ` or `Bearer `' parameters: IdParam: in: path name: id required: true schema: type: integer format: uint description: Resource ID CategoryIdParam: in: path name: category_id required: true schema: type: integer format: uint description: Category ID responses: Error: description: Error response content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' ValidationError: description: Validation error with field details content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Authentication required or token invalid content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Forbidden: description: Access denied content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' schemas: # ========================================================================= # Common # ========================================================================= ErrorResponse: type: object required: [error] properties: error: type: string description: Human-readable error message (may be localized) details: type: object additionalProperties: type: string description: Field-level validation errors (field name -> message) MessageResponse: type: object required: [message] properties: message: type: string # ========================================================================= # Auth Requests # ========================================================================= LoginRequest: type: object properties: username: type: string description: Username (required if email not provided) email: type: string format: email description: Email (required if username not provided) password: type: string minLength: 1 required: [password] RegisterRequest: type: object required: [username, email, password] properties: username: type: string minLength: 3 maxLength: 150 email: type: string format: email maxLength: 254 password: type: string minLength: 8 first_name: type: string maxLength: 150 last_name: type: string maxLength: 150 ForgotPasswordRequest: type: object required: [email] properties: email: type: string format: email VerifyResetCodeRequest: type: object required: [email, code] properties: email: type: string format: email code: type: string minLength: 6 maxLength: 6 ResetPasswordRequest: type: object required: [reset_token, new_password] properties: reset_token: type: string new_password: type: string minLength: 8 UpdateProfileRequest: type: object properties: email: type: string format: email maxLength: 254 nullable: true first_name: type: string maxLength: 150 nullable: true last_name: type: string maxLength: 150 nullable: true VerifyEmailRequest: type: object required: [code] properties: code: type: string minLength: 6 maxLength: 6 AppleSignInRequest: type: object required: [id_token, user_id] properties: id_token: type: string description: Apple identity token (JWT) user_id: type: string description: Apple user ID (sub claim) email: type: string format: email nullable: true description: May be nil or private relay address first_name: type: string nullable: true last_name: type: string nullable: true GoogleSignInRequest: type: object required: [id_token] properties: id_token: type: string description: Google ID token from Credential Manager # ========================================================================= # Auth Responses # ========================================================================= UserResponse: type: object properties: id: type: integer format: uint username: type: string email: type: string format: email first_name: type: string last_name: type: string is_active: type: boolean verified: type: boolean date_joined: type: string format: date-time last_login: type: string format: date-time nullable: true UserProfileResponse: type: object properties: id: type: integer format: uint user_id: type: integer format: uint verified: type: boolean bio: type: string phone_number: type: string date_of_birth: type: string format: date-time nullable: true profile_picture: type: string LoginResponse: type: object required: [token, user] properties: token: type: string user: $ref: '#/components/schemas/UserResponse' RegisterResponse: type: object required: [token, user, message] properties: token: type: string user: $ref: '#/components/schemas/UserResponse' message: type: string SocialSignInResponse: type: object required: [token, user, is_new_user] properties: token: type: string user: $ref: '#/components/schemas/UserResponse' is_new_user: type: boolean CurrentUserResponse: type: object properties: id: type: integer format: uint username: type: string email: type: string format: email first_name: type: string last_name: type: string is_active: type: boolean date_joined: type: string format: date-time last_login: type: string format: date-time nullable: true profile: $ref: '#/components/schemas/UserProfileResponse' VerifyEmailResponse: type: object properties: message: type: string verified: type: boolean VerifyResetCodeResponse: type: object properties: message: type: string reset_token: type: string # ========================================================================= # Static Data / Lookups # ========================================================================= SeededDataResponse: type: object properties: residence_types: type: array items: $ref: '#/components/schemas/ResidenceTypeResponse' task_categories: type: array items: $ref: '#/components/schemas/TaskCategoryResponse' task_priorities: type: array items: $ref: '#/components/schemas/TaskPriorityResponse' task_frequencies: type: array items: $ref: '#/components/schemas/TaskFrequencyResponse' contractor_specialties: type: array items: $ref: '#/components/schemas/ContractorSpecialtyResponse' task_templates: $ref: '#/components/schemas/TaskTemplatesGroupedResponse' ResidenceTypeResponse: type: object properties: id: type: integer format: uint name: type: string TaskCategoryResponse: type: object properties: id: type: integer format: uint name: type: string description: type: string icon: type: string color: type: string display_order: type: integer TaskPriorityResponse: type: object properties: id: type: integer format: uint name: type: string level: type: integer color: type: string display_order: type: integer TaskFrequencyResponse: type: object properties: id: type: integer format: uint name: type: string days: type: integer nullable: true description: Number of days between occurrences (null for one-time) display_order: type: integer ContractorSpecialtyResponse: type: object properties: id: type: integer format: uint name: type: string description: type: string icon: type: string display_order: type: integer TaskTemplateResponse: type: object properties: id: type: integer format: uint title: type: string description: type: string category_id: type: integer format: uint nullable: true category: $ref: '#/components/schemas/TaskCategoryResponse' frequency_id: type: integer format: uint nullable: true frequency: $ref: '#/components/schemas/TaskFrequencyResponse' icon_ios: type: string icon_android: type: string tags: type: array items: type: string display_order: type: integer is_active: type: boolean created_at: type: string format: date-time updated_at: type: string format: date-time TaskTemplateCategoryGroup: type: object properties: category_name: type: string category_id: type: integer format: uint nullable: true templates: type: array items: $ref: '#/components/schemas/TaskTemplateResponse' count: type: integer TaskTemplatesGroupedResponse: type: object properties: categories: type: array items: $ref: '#/components/schemas/TaskTemplateCategoryGroup' total_count: type: integer # ========================================================================= # Residence # ========================================================================= CreateResidenceRequest: type: object required: [name] properties: name: type: string minLength: 1 maxLength: 200 property_type_id: type: integer format: uint nullable: true street_address: type: string maxLength: 255 apartment_unit: type: string maxLength: 50 city: type: string maxLength: 100 state_province: type: string maxLength: 100 postal_code: type: string maxLength: 20 country: type: string maxLength: 100 bedrooms: type: integer nullable: true bathrooms: type: string description: Decimal value (e.g. "2.5") nullable: true square_footage: type: integer nullable: true lot_size: type: string description: Decimal value nullable: true year_built: type: integer nullable: true description: type: string purchase_date: type: string format: date-time nullable: true purchase_price: type: string description: Decimal value (e.g. "350000.00") nullable: true is_primary: type: boolean nullable: true UpdateResidenceRequest: type: object properties: name: type: string minLength: 1 maxLength: 200 nullable: true property_type_id: type: integer format: uint nullable: true street_address: type: string maxLength: 255 nullable: true apartment_unit: type: string maxLength: 50 nullable: true city: type: string maxLength: 100 nullable: true state_province: type: string maxLength: 100 nullable: true postal_code: type: string maxLength: 20 nullable: true country: type: string maxLength: 100 nullable: true bedrooms: type: integer nullable: true bathrooms: type: string nullable: true square_footage: type: integer nullable: true lot_size: type: string nullable: true year_built: type: integer nullable: true description: type: string nullable: true purchase_date: type: string format: date-time nullable: true purchase_price: type: string nullable: true is_primary: type: boolean nullable: true JoinWithCodeRequest: type: object required: [code] properties: code: type: string minLength: 6 maxLength: 6 GenerateShareCodeRequest: type: object properties: expires_in_hours: type: integer description: Hours until code expires (default 24) ResidenceUserResponse: type: object properties: id: type: integer format: uint username: type: string email: type: string format: email first_name: type: string last_name: type: string ResidenceResponse: type: object properties: id: type: integer format: uint owner_id: type: integer format: uint owner: $ref: '#/components/schemas/ResidenceUserResponse' users: type: array items: $ref: '#/components/schemas/ResidenceUserResponse' name: type: string property_type_id: type: integer format: uint nullable: true property_type: $ref: '#/components/schemas/ResidenceTypeResponse' street_address: type: string apartment_unit: type: string city: type: string state_province: type: string postal_code: type: string country: type: string bedrooms: type: integer nullable: true bathrooms: type: string nullable: true description: Decimal square_footage: type: integer nullable: true lot_size: type: string nullable: true description: Decimal year_built: type: integer nullable: true description: type: string purchase_date: type: string format: date-time nullable: true purchase_price: type: string nullable: true description: Decimal is_primary: type: boolean is_active: type: boolean overdue_count: type: integer created_at: type: string format: date-time updated_at: type: string format: date-time TotalSummary: type: object properties: total_residences: type: integer total_tasks: type: integer total_pending: type: integer total_overdue: type: integer tasks_due_next_week: type: integer tasks_due_next_month: type: integer MyResidencesResponse: type: object properties: residences: type: array items: $ref: '#/components/schemas/ResidenceResponse' ResidenceWithSummaryResponse: type: object properties: data: $ref: '#/components/schemas/ResidenceResponse' summary: $ref: '#/components/schemas/TotalSummary' ResidenceDeleteWithSummaryResponse: type: object properties: data: type: string summary: $ref: '#/components/schemas/TotalSummary' ShareCodeResponse: type: object properties: id: type: integer format: uint code: type: string residence_id: type: integer format: uint created_by_id: type: integer format: uint is_active: type: boolean expires_at: type: string format: date-time nullable: true created_at: type: string format: date-time JoinResidenceResponse: type: object properties: message: type: string residence: $ref: '#/components/schemas/ResidenceResponse' summary: $ref: '#/components/schemas/TotalSummary' GenerateShareCodeResponse: type: object properties: message: type: string share_code: $ref: '#/components/schemas/ShareCodeResponse' SharePackageResponse: type: object properties: share_code: type: string residence_name: type: string shared_by: type: string expires_at: type: string format: date-time nullable: true # ========================================================================= # Task # ========================================================================= CreateTaskRequest: type: object required: [residence_id, title] properties: residence_id: type: integer format: uint title: type: string minLength: 1 maxLength: 200 description: type: string category_id: type: integer format: uint nullable: true priority_id: type: integer format: uint nullable: true frequency_id: type: integer format: uint nullable: true custom_interval_days: type: integer nullable: true description: For "Custom" frequency, user-specified interval in days in_progress: type: boolean default: false assigned_to_id: type: integer format: uint nullable: true due_date: type: string description: Accepts "2025-11-27" or "2025-11-27T00:00:00Z" nullable: true estimated_cost: type: string description: Decimal value (e.g. "150.00") nullable: true contractor_id: type: integer format: uint nullable: true UpdateTaskRequest: type: object properties: title: type: string minLength: 1 maxLength: 200 nullable: true description: type: string nullable: true category_id: type: integer format: uint nullable: true priority_id: type: integer format: uint nullable: true frequency_id: type: integer format: uint nullable: true custom_interval_days: type: integer nullable: true in_progress: type: boolean nullable: true assigned_to_id: type: integer format: uint nullable: true due_date: type: string nullable: true estimated_cost: type: string nullable: true actual_cost: type: string nullable: true contractor_id: type: integer format: uint nullable: true TaskUserResponse: type: object properties: id: type: integer format: uint username: type: string email: type: string format: email first_name: type: string last_name: type: string TaskResponse: type: object properties: id: type: integer format: uint residence_id: type: integer format: uint created_by_id: type: integer format: uint created_by: $ref: '#/components/schemas/TaskUserResponse' assigned_to_id: type: integer format: uint nullable: true assigned_to: $ref: '#/components/schemas/TaskUserResponse' title: type: string description: type: string category_id: type: integer format: uint nullable: true category: $ref: '#/components/schemas/TaskCategoryResponse' priority_id: type: integer format: uint nullable: true priority: $ref: '#/components/schemas/TaskPriorityResponse' frequency_id: type: integer format: uint nullable: true frequency: $ref: '#/components/schemas/TaskFrequencyResponse' in_progress: type: boolean due_date: type: string format: date nullable: true next_due_date: type: string format: date nullable: true description: For recurring tasks, updated after each completion estimated_cost: type: string nullable: true description: Decimal actual_cost: type: string nullable: true description: Decimal contractor_id: type: integer format: uint nullable: true is_cancelled: type: boolean is_archived: type: boolean parent_task_id: type: integer format: uint nullable: true completion_count: type: integer kanban_column: type: string description: "Computed column name: overdue, due_soon, upcoming, completed, in_progress, archived, cancelled" enum: [overdue, due_soon, upcoming, completed, in_progress, archived, cancelled] created_at: type: string format: date-time updated_at: type: string format: date-time KanbanColumnResponse: type: object properties: name: type: string description: Column identifier display_name: type: string description: Human-readable column name button_types: type: array items: type: string description: Available action button types for tasks in this column icons: type: object additionalProperties: type: string description: Platform-specific icon names (ios, android) color: type: string description: Hex color for the column tasks: type: array items: $ref: '#/components/schemas/TaskResponse' count: type: integer KanbanBoardResponse: type: object properties: columns: type: array items: $ref: '#/components/schemas/KanbanColumnResponse' days_threshold: type: integer description: Number of days used for "due soon" calculation (default 30) residence_id: type: string description: '"all" or the specific residence ID' TaskWithSummaryResponse: type: object properties: data: $ref: '#/components/schemas/TaskResponse' summary: $ref: '#/components/schemas/TotalSummary' DeleteWithSummaryResponse: type: object properties: data: type: string summary: $ref: '#/components/schemas/TotalSummary' # ========================================================================= # Task Completion # ========================================================================= CreateTaskCompletionRequest: type: object required: [task_id] properties: task_id: type: integer format: uint completed_at: type: string format: date-time nullable: true description: Defaults to now if not provided notes: type: string actual_cost: type: string nullable: true description: Decimal rating: type: integer minimum: 1 maximum: 5 nullable: true image_urls: type: array items: type: string description: URLs of uploaded images to attach UpdateTaskCompletionRequest: type: object properties: notes: type: string nullable: true actual_cost: type: string nullable: true rating: type: integer minimum: 1 maximum: 5 nullable: true image_urls: type: array items: type: string description: Replaces all existing image associations TaskCompletionImageResponse: type: object properties: id: type: integer format: uint image_url: type: string description: Direct S3 URL (may require auth) media_url: type: string description: "Authenticated proxy endpoint: /api/media/completion-image/{id}" caption: type: string TaskCompletionResponse: type: object properties: id: type: integer format: uint task_id: type: integer format: uint completed_by: $ref: '#/components/schemas/TaskUserResponse' completed_at: type: string format: date-time notes: type: string actual_cost: type: string nullable: true description: Decimal rating: type: integer nullable: true images: type: array items: $ref: '#/components/schemas/TaskCompletionImageResponse' created_at: type: string format: date-time task: $ref: '#/components/schemas/TaskResponse' description: Included after create/quick-complete with updated task state TaskCompletionWithSummaryResponse: type: object properties: data: $ref: '#/components/schemas/TaskCompletionResponse' summary: $ref: '#/components/schemas/TotalSummary' # ========================================================================= # Contractor # ========================================================================= CreateContractorRequest: type: object required: [name] properties: residence_id: type: integer format: uint nullable: true name: type: string minLength: 1 maxLength: 200 company: type: string maxLength: 200 phone: type: string maxLength: 20 email: type: string format: email maxLength: 254 website: type: string maxLength: 200 notes: type: string street_address: type: string maxLength: 255 city: type: string maxLength: 100 state_province: type: string maxLength: 100 postal_code: type: string maxLength: 20 specialty_ids: type: array items: type: integer format: uint rating: type: number format: double nullable: true is_favorite: type: boolean nullable: true UpdateContractorRequest: type: object properties: name: type: string minLength: 1 maxLength: 200 nullable: true company: type: string maxLength: 200 nullable: true phone: type: string maxLength: 20 nullable: true email: type: string format: email maxLength: 254 nullable: true website: type: string maxLength: 200 nullable: true notes: type: string nullable: true street_address: type: string maxLength: 255 nullable: true city: type: string maxLength: 100 nullable: true state_province: type: string maxLength: 100 nullable: true postal_code: type: string maxLength: 20 nullable: true specialty_ids: type: array items: type: integer format: uint rating: type: number format: double nullable: true is_favorite: type: boolean nullable: true residence_id: type: integer format: uint nullable: true ContractorResponse: type: object properties: id: type: integer format: uint residence_id: type: integer format: uint nullable: true created_by_id: type: integer format: uint added_by: type: integer format: uint description: Alias for created_by_id (KMM compatibility) created_by: type: object properties: id: type: integer format: uint username: type: string first_name: type: string last_name: type: string name: type: string company: type: string phone: type: string email: type: string website: type: string notes: type: string street_address: type: string city: type: string state_province: type: string postal_code: type: string specialties: type: array items: $ref: '#/components/schemas/ContractorSpecialtyResponse' rating: type: number format: double nullable: true is_favorite: type: boolean is_active: type: boolean task_count: type: integer created_at: type: string format: date-time updated_at: type: string format: date-time ToggleFavoriteResponse: type: object properties: message: type: string is_favorite: type: boolean # ========================================================================= # Document # ========================================================================= DocumentType: type: string enum: [general, warranty, receipt, contract, insurance, manual] CreateDocumentRequest: type: object required: [residence_id, title] properties: residence_id: type: integer format: uint title: type: string minLength: 1 maxLength: 200 description: type: string document_type: $ref: '#/components/schemas/DocumentType' file_url: type: string maxLength: 500 file_name: type: string maxLength: 255 file_size: type: integer format: int64 nullable: true mime_type: type: string maxLength: 100 purchase_date: type: string format: date-time nullable: true expiry_date: type: string format: date-time nullable: true purchase_price: type: string nullable: true description: Decimal vendor: type: string maxLength: 200 serial_number: type: string maxLength: 100 model_number: type: string maxLength: 100 task_id: type: integer format: uint nullable: true image_urls: type: array items: type: string description: URLs of uploaded images to attach UpdateDocumentRequest: type: object properties: title: type: string minLength: 1 maxLength: 200 nullable: true description: type: string nullable: true document_type: $ref: '#/components/schemas/DocumentType' file_url: type: string maxLength: 500 nullable: true file_name: type: string maxLength: 255 nullable: true file_size: type: integer format: int64 nullable: true mime_type: type: string maxLength: 100 nullable: true purchase_date: type: string format: date-time nullable: true expiry_date: type: string format: date-time nullable: true purchase_price: type: string nullable: true vendor: type: string maxLength: 200 nullable: true serial_number: type: string maxLength: 100 nullable: true model_number: type: string maxLength: 100 nullable: true task_id: type: integer format: uint nullable: true DocumentImageResponse: type: object properties: id: type: integer format: uint image_url: type: string media_url: type: string description: "Authenticated proxy endpoint: /api/media/document-image/{id}" caption: type: string DocumentResponse: type: object properties: id: type: integer format: uint residence_id: type: integer format: uint residence: type: integer format: uint description: Alias for residence_id (KMM compatibility) created_by_id: type: integer format: uint created_by: type: object properties: id: type: integer format: uint username: type: string first_name: type: string last_name: type: string title: type: string description: type: string document_type: $ref: '#/components/schemas/DocumentType' file_url: type: string media_url: type: string description: "Authenticated proxy endpoint: /api/media/document/{id}" file_name: type: string file_size: type: integer format: int64 nullable: true mime_type: type: string purchase_date: type: string format: date-time nullable: true expiry_date: type: string format: date-time nullable: true purchase_price: type: string nullable: true description: Decimal vendor: type: string serial_number: type: string model_number: type: string task_id: type: integer format: uint nullable: true is_active: type: boolean images: type: array items: $ref: '#/components/schemas/DocumentImageResponse' created_at: type: string format: date-time updated_at: type: string format: date-time # ========================================================================= # Notifications # ========================================================================= NotificationType: type: string enum: - task_due_soon - task_overdue - task_completed - task_assigned - residence_shared - warranty_expiring Notification: type: object properties: id: type: integer format: uint user_id: type: integer format: uint notification_type: $ref: '#/components/schemas/NotificationType' title: type: string body: type: string task_id: type: integer format: uint nullable: true data: type: string description: JSON string with additional data sent: type: boolean sent_at: type: string format: date-time nullable: true read: type: boolean read_at: type: string format: date-time nullable: true error_message: type: string created_at: type: string format: date-time updated_at: type: string format: date-time NotificationListResponse: type: object properties: count: type: integer results: type: array items: $ref: '#/components/schemas/Notification' NotificationPreference: type: object properties: id: type: integer format: uint user_id: type: integer format: uint task_due_soon: type: boolean task_overdue: type: boolean task_completed: type: boolean task_assigned: type: boolean residence_shared: type: boolean warranty_expiring: type: boolean daily_digest: type: boolean email_task_completed: type: boolean task_due_soon_hour: type: integer nullable: true description: UTC hour (0-23) for task due soon notifications task_overdue_hour: type: integer nullable: true description: UTC hour (0-23) for task overdue notifications warranty_expiring_hour: type: integer nullable: true daily_digest_hour: type: integer nullable: true UpdatePreferencesRequest: type: object properties: task_due_soon: type: boolean nullable: true task_overdue: type: boolean nullable: true task_completed: type: boolean nullable: true task_assigned: type: boolean nullable: true residence_shared: type: boolean nullable: true warranty_expiring: type: boolean nullable: true daily_digest: type: boolean nullable: true email_task_completed: type: boolean nullable: true task_due_soon_hour: type: integer nullable: true task_overdue_hour: type: integer nullable: true warranty_expiring_hour: type: integer nullable: true daily_digest_hour: type: integer nullable: true RegisterDeviceRequest: type: object required: [device_id, registration_id, platform] properties: name: type: string device_id: type: string registration_id: type: string description: APNs device token or FCM registration token platform: type: string enum: [ios, android] UnregisterDeviceRequest: type: object required: [registration_id] properties: registration_id: type: string platform: type: string enum: [ios, android] default: ios DeviceResponse: type: object properties: id: type: integer format: uint name: type: string device_id: type: string registration_id: type: string platform: type: string enum: [ios, android] active: type: boolean date_created: type: string format: date-time # ========================================================================= # Subscriptions # ========================================================================= SubscriptionResponse: type: object properties: tier: type: string enum: [free, pro] subscribed_at: type: string format: date-time nullable: true expires_at: type: string format: date-time nullable: true auto_renew: type: boolean cancelled_at: type: string format: date-time nullable: true platform: type: string description: "ios or android" is_active: type: boolean is_pro: type: boolean UsageResponse: type: object properties: properties_count: type: integer format: int64 tasks_count: type: integer format: int64 contractors_count: type: integer format: int64 documents_count: type: integer format: int64 TierLimitsClientResponse: type: object properties: properties: type: integer nullable: true description: "null means unlimited" tasks: type: integer nullable: true contractors: type: integer nullable: true documents: type: integer nullable: true SubscriptionStatusResponse: type: object properties: subscribed_at: type: string format: date-time nullable: true expires_at: type: string format: date-time nullable: true auto_renew: type: boolean usage: $ref: '#/components/schemas/UsageResponse' limits: type: object additionalProperties: $ref: '#/components/schemas/TierLimitsClientResponse' description: 'Map of tier name -> limits (e.g. {"free": {...}, "pro": {...}})' limitations_enabled: type: boolean description: Whether subscription limitations are currently enforced UpgradeTriggerResponse: type: object properties: trigger_key: type: string title: type: string message: type: string promo_html: type: string button_text: type: string FeatureBenefit: type: object properties: id: type: integer format: uint feature_name: type: string free_tier_text: type: string pro_tier_text: type: string display_order: type: integer is_active: type: boolean Promotion: type: object properties: id: type: integer format: uint promotion_id: type: string title: type: string message: type: string link: type: string nullable: true start_date: type: string format: date-time end_date: type: string format: date-time target_tier: type: string enum: [free, pro] is_active: type: boolean ProcessPurchaseRequest: type: object required: [platform] properties: receipt_data: type: string description: iOS StoreKit 1 receipt or StoreKit 2 JWS transaction_id: type: string description: iOS StoreKit 2 transaction ID purchase_token: type: string description: Android Google Play purchase token product_id: type: string description: Android product ID (helps identify subscription) platform: type: string enum: [ios, android] # ========================================================================= # Uploads # ========================================================================= UploadResult: type: object properties: url: type: string description: URL of the uploaded file file_name: type: string file_size: type: integer format: int64 mime_type: type: string